A newer version is available at the documentation home.

Preface

This manual provides the reference information for the CUBA platform and covers the most important topics of developing business applications with it.

Knowledge of the following technologies is required to use the platform:

  • Java Standard Edition

  • Relational databases (SQL, DDL)

Additionally, knowledge of the following technologies and frameworks will be helpful to get a deeper understanding of the platform:

This manual and other documentation related to the CUBA platform can be found at www.cuba-platform.com/manual. Video materials and presentations that can help you to understand the platform are available at www.cuba-platform.com/tutorials. You can also check out online demo applications at www.cuba-platform.com/online-demo.

If you have any suggestions for improvement of this Manual, please contact support at www.cuba-platform.com/support. When reporting errors in the documentation, please indicate the chapter and surrounding text to point the error.

1. Introduction

This chapter provides information about the CUBA platform features and requirements.

1.1. Overview

CUBA platform is an ideal tool for development teams working on line-of-business applications, typically having extensive data model, hundreds of screens and complex business logic.

Based on a mainstream technology stack, CUBA platform brings unparalleled productivity by utilizing a rich set of ready to use data-aware components, extensive scaffolding, visual interface designer and hot deploy.

Open architecture allows a developer to customize any part of the framework, providing high levels of control and flexibility. Developers have the freedom to use popular Java IDEs and have full access to the source code.

CUBA applications fit seamlessly into the corporate IT environment, supporting major databases and application servers, as well as popular aPaaS clouds. Streamlined clustered deployment ensures scalability and failover, while a generic REST API enables easy integration with other systems.

1.2. Technical Requirements

Minimum requirements for development using CUBA platform:

  • Memory – 4 GB

  • Hard drive space – 5 GB

  • Operating system: Microsoft Windows, Linux or macOS

1.3. Release Notes

CUBA platform changelog is available at http://files.cuba-platform.com/cuba/release-notes/6.6

2. Installation and Setup

Minimum software requirements are as follows:

  • Java SE Development Kit (JDK) 8. It is recommended that you use Oracle Java HotSpot VM.

    In order to build and run projects outside Studio, you need to set the path to the JDK root directory in the JAVA_HOME environment variable, e.g. C:\Program Files\Java\jdk1.8.0_60. On Windows, you can do this at ComputerPropertiesAdvanced System SettingsAdvancedEnvironment variables. The value of the variable should be added to the System variables list.

  • Java IDE: IntelliJ IDEA 13+ or Eclipse 4.3+. We recommend using IntelliJ IDEA (Community or Ultimate).

In the most basic scenario, the built-in HyperSQL (http://hsqldb.org) can be used as the database server. This is sufficient for exploring the platform capabilities and application prototyping. For building production applications, it is recommended to install and use one of the full-featured DBMS supported by the platform, like PostgreSQL for instance.

The web interface of the platform-based applications supports all popular browsers, including Google Chrome, Mozilla Firefox, Safari, Opera 15+, Internet Explorer 9+, Microsoft Edge.

Warning

Please make sure your environment does not contain CATALINA_HOME, CATALINA_BASE and CLASSPATH variables. They may cause problems starting Apache Tomcat web server which is used at development time. Reboot your computer after removing the variables.

2.1. CUBA Studio Installation

Prerequisites
  • Make sure that Java SE Development Kit (JDK) 8 is installed by running the following command in the console:

    java -version

    The command should return the Java version, e.g. 1.8.0_60.

  • If you are using OpenJDK on Linux, install OpenJFX, for example:

    sudo apt-get install openjfx

  • If you connect to the internet via a proxy server, some Java system properties must be passed to the JVM running Studio and Gradle. These properties are explained here: http://docs.oracle.com/javase/8/docs/technotes/guides/net/proxies.html (see properties for HTTP and HTTPS protocols).

    It is recommended to set these properties system-wide in the JAVA_OPTS environment variable. The Studio launch script passes JAVA_OPTS to the Java executable.

Fresh installation of CUBA Studio
  1. Download an appropriate installer or ZIP archive from https://www.cuba-platform.com/download.

  2. Run the installer or unzip the archive to a local directory, e.g. c:/work/studio

  3. Launch the installed application or open the command line, go to bin directory and run studio

  4. In the CUBA Studio Server window, enter the following parameters:

    • Java home − JDK installation to be used for building and running projects. If you have set the JAVA_HOME environment variable as described in the beginning of this chapter, it will appear in this field. Otherwise, Studio will try to find your Java installation itself.

    • Advanced button opens a dialog that allows you to enter a path to Gradle home directory. Leave it empty; in this case, the required Gradle distribution will be downloaded automatically.

      If you want to use a local Gradle distribution, enter the path to the respective directory. For project build system to work correctly, Gradle 3.4 is required.

    • Server port − CUBA Studio server port (the default port is 8111).

    • IDE port − IDE plugin listening port (the default port is 48561).

    • Repository − binary artifacts repository URL and authentication parameters.

      studio server window

    The following options are also available:

    • Check for updates - check for new versions on every start.

    • Help language - built-in help language.

    • Offline - enable working with projects without an Internet connection, provided that all the required libraries have been previously downloaded from the repository.

    • Send anonymous statistics and crash reports - enable Studio to send error statistics to developers.

    • Enable remote connection - by default, it is assumed that Studio runs on localhost. Select this checkbox if you need to connect to this Studio instance from a remote host.

    • Silent startup - if selected, the Studio server starts in tray and opens UI in default browser automatically. This option is available only for Windows and macOS.

  5. Click Start to run the Studio server.

    The server will download, run, and connect to the Gradle daemon. This may take a significant amount of time on first startup; on subsequent launches, this will take just a few seconds.

    After that, the web server will be started, and the URL of the Studio interface will appear in the URL field. By clicking , you can open the address in your default web browser; by clicking Copy you can copy the address to the clipboard.

  6. Open the specified address in the web browser.

  7. In the Studio web interface, click Create new to create a new project, or Import to add an existing one to the Recent list.

  8. Once the project is opened, the Studio will download the source code of the platform components and save it to the local folder. Before building the project, it is recommended to wait until the download is finished and make sure that the background task indicator in the bottom left corner has faded out.

Updating CUBA Studio

If you are updating Studio to a newer bug-fix version (e.g. from 6.5.0 to 6.5.1), install it to the existing folder, e.g. on Windows it would be C:\Program Files (x86)\CUBA Studio 6.5. When installing a new minor or major version, use a separate folder, e.g. CUBA Studio 6.6.

If installed from Windows EXE installer or ZIP archive, Studio supports auto-update on newer bug-fix releases. Update files are saved in the ~/.haulmont/studio/update folder. In case of any problems with the new version, you can remove the update files and Studio will revert to the version installed manually.

Auto-update does not work for minor and major releases and if Studio was installed from macOS DMG. In this case, you should download new installer and run it manually.

2.2. IDE Integration

Take the following steps to integrate Studio with IntelliJ IDEA or Eclipse:

  1. Open or create a new project in the Studio.

  2. Switch to Project properties section and click Edit. Select the required Java IDE by checking IntelliJ IDEA or Eclipse.

  3. Select Build > Create or update <IDE> project files in the Studio menu. The corresponding files will be created in the project directory.

  4. For IntelliJ IDEA integration:

    1. Run IntelliJ IDEA 13+ and install CUBA Framework Integration plugin, from the plugin repository: File > Settings > Plugins > Browse Repositories.

  5. For Eclipse integration:

    1. Run Eclipse 4.3+, open Help > Install New Software, add http://files.cuba-platform.com/eclipse-update-site repository and install the CUBA Plugin.

    2. In the CUBA section of the Window > Preferences menu, check Studio Integration Enabled, and click OK.

Please note that IDE: on port 48561 label has appeared in the bottom left corner of the Studio. Now the corresponding source code files will be opened in IDE when you click IDE buttons in the Studio.

3. Quick Start

This section describes the process of creating an application using CUBA Studio. Similar information is provided in the videos available at www.cuba-platform.com/quickstart.

Make sure that the necessary software is already installed and set up on your computer, see Installation and Setup.

Key stages of our application development:

  1. Data model development including creation of entities describing application domain and corresponding database tables.

  2. Development of the user interface screens enabling to create, view, update and delete data model entities.

3.1. Application Details

The application should maintain information about the customers and their orders.

A customer has the following attributes:

  • Name

  • Email

Order attributes:

  • Reference to a customer

  • Date

  • Amount

quick start 1

The application UI should contain:

  • Customers browser screen;

  • Customer editor screen, containing as well the list of this customer’s orders;

  • General orders browser screen;

  • Order editor screen.

3.2. Creating a Project

  1. Start CUBA Studio and open its web interface (See CUBA Studio Installation).

  2. Click Create new.

  3. Specify the name of the new project in the Project name field of the New project window – for example, sales. The name should contain only Latin letters, numbers and underscores. Think carefully on the project name at this stage, as changing it later on will require complex manual intervention.

  4. The following fields below will be automatically populated:

    • Project path – the path to the new project directory. You can select the directory manually by clicking the …​ button next to the field. The Select folder window will appear with the list of folders n your hard drive. You can select one of those, or create a new directory by clicking the + button.

    • Project namespace – the namespace which will be used as a prefix for entity names and database tables. The namespace can consist of Latin letters only and should be as short as possible. For example, if the project name is sales_2, the namespace can be sales or sal.

    • Root package − the root package of Java classes. It can be adjusted later, but the classes generated at project creation will not be moved.

    • Platform version – the platform version used in the project. The platform artifacts will be automatically downloaded from the repository on project build.

  5. Click OK. Empty project will be created in the specified sales directory and the main Studio window will open.

  6. Assemble the project: select option Build > Assemble project in the Studio main menu. At this stage all required libraries will be downloaded and project artifacts will be assembled in build subdirectories of the modules.

  7. Create the database on the local HyperSQL server: select option Run > Create database in the menu. The database name is the same as project namespace by default.

  8. Select RunDeploy menu option. Tomcat server with the deployed application will be installed in the project deploy subdirectory.

  9. Select RunStart application server option. The link next to the Web application caption on the status panel will become available in a few seconds so you will be able to open the application directly from Studio.

    The username and password are admin / admin.

    The running application contains two main menu items (Administration and Help), as well as security and administration subsystems functionality.

3.3. Creating Entities

Let’s create the Customer entity class.

  • Go to the DATA MODEL tab in the navigation section and click NewEntity. The New entity dialog window will appear.

  • Enter the name of the entity class – Customer – in the Class name field.

    qs create customer entity
  • Click OK. The entity designer page will be displayed in the workspace.

    qs customer entity
  • The entity name and the database table name will be automatically generated in the Name and the Table fields respectively.

  • Leave the existing value – StandardEntity - in the Parent class field.

  • Leave the Inheritance strategy field blank.

Next, let’s create entity attributes. To do this, click the New button below the Attributes table.

  • Create attribute window will appear. Enter the name of the entity attribute − name, in the Name field. Select DATATYPE value in the Attribute type list, specify String attribute type in the Type field and then set the length of the text attribute to 100 characters in the Length field. Check the Mandatory box. The name of the database table column will be automatically generated in the Column field.

    qs new attribute

    Click Add to add the attribute.

  • email attribute is created in the same way but the value in Length field should be set to 50.

After creating the attributes, go to the INSTANCE NAME tab in the entity designer to specify the Name pattern. Select the name attribute in the Available attributes list and move it to the Name pattern attributes list by clicking the button with the right arrow on it.

qs customer instance name

Customer entity creation is now complete. Click OK on the top panel to save the changes and close the page.

Let’s create the Order entity.

Click NewEntity on the DATA MODEL tab. Enter the Class nameOrder. The entity should have the following attributes:

  • Namecustomer, Attribute typeASSOCIATION, TypeCustomer, CardinalityMANY_TO_ONE.

  • Namedate, Attribute typeDATATYPE, TypeDate. Check Mandatory box for date attribute.

  • Nameamount, Attribute typeDATATYPE, TypeBigDecimal.

3.4. Creating Database Tables

It is sufficient to click Generate DB scripts button in DATA MODEL tab on the navigation panel to create database tables. After that, DATABASE SCRIPTS page will open. Both incremental DB update scripts from the current state (UPDATE SCRIPTS) and initial DB creation scripts (INIT TABLES, INIT TABLES, INIT DATA) will be generated on this page.

qs generate db scripts

Click Save and close button to save the generated scripts. To run update scripts, stop the running application using the RunStop application server command, then select RunUpdate database.

3.5. Creating User Interface Screens

Now we will create screens for customers and orders data management.

3.5.1. Screens for Customer

Select Customer entity in the DATA MODEL tab on the navigation panel to create standard screens for viewing and editing Customers. Click NewGeneric UI screen at the top of the section. After that, the template browser page will appear.

Select Entity browser and editor screens in the list of available templates.

qs create customer screens

All fields in this dialog are already populated with default values, there is no need to change them. Click Create and then Close buttons.

customer-browse.xml and customer-edit.xml items will appear in Web Module on GENERIC UI tab of the navigation panel.

3.5.2. Order Screens

Order entity has the following distinction: since one of the attributes is the Order.customer reference attribute, you should define a view including this attribute (standard _local view does not include reference attributes).

Go to the DATA MODEL tab on the navigation panel, select the Order entity and click the NewView button. View designer page will open. Enter order-with-customer as the view name, click on customer attribute and select _minimal view for the Customer entity on the panel on the right.

qs order view

Click OK on the top panel.

After that, select the Order entity and click NewGeneric UI screen.

Select order-with-customer in the View fields for both browser and editor templates and click Create.

qs create order screens

order-edit.xml and order-browse.xml items will appear in the Web Module on the GENERIC UI tab of the navigation panel.

3.5.3. Application Menu

At the moment of their creation, the screens were added to the application menu item of the default application menu. Let’s rename it. Switch to the GENERIC UI tab on the navigation panel and click Open web menu. The MENU DESIGNER page will open. Select the application menu item to edit its properties.

Enter the new value of the menu identifier − shop − in the Id field, then click OK on the top panel.

3.5.4. Customer Editor With a List of Orders

Do the following to display the list of Orders in the Customer’s edit screen:

  • Go to the GENERIC UI tab on the navigation panel. Choose customer-edit.xml screen and click Edit.

  • Go to the DATASOURCES tab on the screen designer page and click New.

  • Select the newly created datasource in the list. Its attributes will appear in the right part of the page.

  • Specify collectionDatasource in the Type field.

  • Select Order entity in the Entity list.

  • The data source identifier − ordersDs - will be automatically generated in Id field.

  • Select _local view in the View list.

  • Add the WHERE clause to the query generated in the Query field:

    select o from sales$Order o where o.customer.id = :ds$customerDs order by o.date

    The query contains orders selection criterion with ds$customerDs parameter. The parameter value named like ds${datasource_name} will contain id of the entity selected in datasource_name datasource at the moment, in this case it is the id of the Customer being edited.

    qs customer screen orders ds
  • Click Apply to save the changes.

  • Next go to the LAYOUT tab in the screen designer and find the Label component in the components palette. Drag this component to the screen components hierarchy panel and place it between fieldGroup and windowActions. Go to the PROPERTIES tab on the properties panel. Enter the label value Orders in the value field.

    qs customer screen label
    Tip

    If the application is intended to be used in multiple languages, use the localization button next to the value field to create the new message msg://orders and define label values in required languages.

  • Drag Table from the components palette to components hierarchy panel and place it between label and windowActions. Select this component in the hierarchy and specify table size in the PROPERTIES tab: set 100% in the width field and 200px in the height field. Choose orderDs from the list of available datasources. Then generate the table identifier using the generate_id button next to the id field: ordersTable.

    qs customer screen table
  • Click OK on the top panel to save the changes in the screen.

3.6. Running the Application

Now let’s see how the created screens look in the actual application. Select RunStart application server.

Log in using default credentials in the login window. Open the ShopCustomers menu item:

qs customer browse
Figure 1. The Customers browser

Click Create and create a new customer:

qs customer edit
Figure 2. The Customer editor screen

Open the ShopOrders menu item:

qs order browse
Figure 3. The Orders browser

Click Create and create a new order, selecting the newly created customer in the Customer field:

qs order edit
Figure 4. The Order editor

The new order is now displayed in the customer’s editor:

qs customer edit 2
Figure 5. The Customer editor

4. Cookbook

This collection of practical recipes for developing on CUBA platform contains examples of implementing typical use cases and solving common problems. The information in each section is organized from basic to advanced topics, so feel free to jump to another section or leave the documentation at any time and start coding.

Most of the sections are accompanied by the sample applications. You can see them online, view their source code on GitHub or download and run locally. You will also see the applications on the Samples tab in Studio.

Tip

The cookbook is a work in progress and will be gradually improved.

4.1. Organizing Business Logic

When you start developing on the platform, one of the first questions is "where should I put my business logic"? Using Studio for creating data model and CRUD screens is simple, but any real project requires some logic beyond CRUD. This section explains how you can effectively organize your business logic depending on your requirements.

Most examples in this section work with the following data model:

business logic model 1

In these examples, we will calculate discounts for customers based on total amount of their purchases.

4.1.1. Business Logic in Controllers

If we want to run the discount calculation when a user clicks a button on the customer’s browser screen, the most straightforward way to accomplish this is to put the calculation logic right in the browser screen controller.

See the Calculate discount button in the demo application and the screen controller implementation: CustomerBrowse.java. Please keep in mind that the provided calculation process is not optimal and see more options in the Loading and Saving Data section.

This approach is acceptable if the logic is invoked from a single point and it is not too complex to fit into a couple of short methods.

4.1.2. Using Client Tier Beans

Let’s complicate the task from the previous section a bit. Now we want to invoke the calculation both from the customer browser and editor screens. To not repeat yourself, we should extract the logic to a common place available for both controllers. It can be a managed bean of the client tier.

A managed bean is a class annotated with the @Component annotation. It can be injected into other beans and screen controllers, or obtained via the AppBeans.get() static method. If the bean has a separate interface, you can access the bean through the interface instead of the class.

Please note that in order to be accessible for screen controllers, the bean must be located in global, gui or web modules of your project. In the former case the bean will be also accessible for the middleware.

See the Calculate discount button on both browser and editor screens of the demo application and the implementation:

using client beans 1

4.1.3. Using Middleware Services

In the previous section we considered the encapsulation of business logic in a managed bean of the client tier. Now we will go further and implement our logic in the most appropriate place: on the middle tier. By doing this, we will achieve the following goals:

  • Our business methods will be available for all types of clients including Polymer UI.

  • We will be able to use APIs available only on the middleware: EntityManager, transactions, etc.

In order to invoke a middleware business method from the client, you need to create a service. Studio can help you to scaffold the service stub:

  • Switch to the Middleware section and click New > Service.

  • Change the service interface name to DiscountService. The bean class and service names will be changed accordingly. Click OK or Apply.

  • Click IDE and open the service interface in your IDE. Create a method and implement it in the service class.

See an example implementation in the demo application:

using services 1
  • CustomerBrowse.java and CustomerEdit.java - screen controllers that invoke the service.

  • DiscountService.java - service interface.

  • DiscountServiceBean.java - service implementation.

  • DiscountCalculator.java - a managed bean of the middle tier which actually calculates discounts. Of course, a service can contain the business logic itself, but we will use this delegate to share logic with entity listeners and JMX beans (see next sections).

    Please note that this bean is different from the one mentioned in the previous section: it is located in the core module and uses EntityManager for loading the amount of purchases from the database.

Let’s now make our business method accessible for external clients through the REST API:

  • Open the service editor in Studio and switch to the REST Methods tab.

  • Select the REST invocation allowed checkbox for the method.

Studio will create the rest-services.xml file and write the method description into it. After restarting the application server you will be able to invoke your business method using HTTP requests. For example, the following GET request should work with our online demo server:

Please note that the demo application allows anonymous access. In the most real-world usage scenarios you need to authenticate prior to executing REST requests.

4.1.4. Using Entity Listeners

Entity listeners allow you to execute your business logic each time an entity is added, updated or removed from the database. For example, we could recalculate the discount for a customer each time an order for this customer is changed.

An entity listener stub can be easily created using Studio:

  • Switch to the Middleware section and click New > Entity listener.

  • Change the class name to OrderEntityListener and select checkboxes for BeforeInsertEntityListener, BeforeUpdateEntityListener and BeforeDeleteEntityListener interfaces.

  • Select Order entity in the Entity type field.

  • Click OK or Apply and open the listener class in your IDE.

See an example implementation in the demo application:

using entity listeners 1
  • OrderEntityListener.java - the entity listener.

  • DiscountCalculator.java - a managed bean of the middle tier which actually calculates discounts. An entity listener can contain the business logic itself, but we will use this delegate to share logic with services and JMX beans.

If you open the Logic in Entity Listeners screen of the demo application, you will see two tables: orders and customers. Create, edit or remove an order, then refresh the customers table, and you will see that the discount of the corresponding customer is changed.

4.1.5. Using JMX Beans

With JMX beans you can expose some administrative functionality of your application without creating a user interface for it. The functionality becomes available via the built-in JMX console and via external JMX tools like jconsole.

In our example with discounts, a user having access to JMX console is able to recalculate discounts for all customers and for a customer with a given id.

Studio cannot help you with scaffolding JMX beans at the moment, so all classes and configuration entries have to be created manually in the IDE.

See an example implementation in the demo application:

using jmx beans 1
  • DiscountsMBean.java - JMX bean interface.

  • Discounts.java - JMX bean implementation.

  • DiscountCalculator.java - a managed bean of the middle tier which is invoked by the JMX bean. A JMX bean can contain the business logic itself, but we will use this delegate to share logic with services and entity listeners.

  • spring.xml - registers the JMX bean.

4.1.6. Running Code on Startup

Sometimes you need to run some code on the application startup, at the moment when all application functionality is already initialized and ready to work. For this, you can use AppContext.Listener.

In this section we demonstrate how to dynamically register an entity listener on application startup. Consider the following task: a project has an Employee entity that is linked one-to-one to the platform’s User entity.

app start recipe 1

If the name attribute of the User entity is changed, for example, through a standard user management screen, the name attribute of the related Employee should change as well. This is a common task for "denormalized" data, which is typically solved using entity listeners. Our case is more complicated, since we need to track changes of the platform’s User entity, and thus we cannot add an entity listener using the @Listeners annotation. So we will add a listener dynamically using the EntityListenerManager bean on application start.

  • AppLifecycle.java - a middleware bean implementing the AppContext.Listener interface with the applicationStarted() and applicationStopped() methods.

  • UserEntityListener.java - an entity listener for the User entity.

As a result, the applicationStarted() method of the AppLifecycle bean will be invoked on the middleware block startup. This method registers the sample_UserEntityListener bean as an entity listener for the User entity.

The onBeforeUpdate() method of the UserEntityListener class will be invoked every time before the changes in the User instances are saved to the database. The method checks if the name attribute exists among the updated attributes. If yes, a related Employee instance is loaded and its name is updated with the new value.

4.2. Modeling Problem Domain

In this section, you can find recipes for the data model design and working with entity attributes.

4.2.1. Assigning Initial Values

There are different ways to assign initial values to the attributes of new entity instances.

4.2.1.1. Entity Fields Initialization

Simple attributes (Boolean, Integer etc.) and enumerations can be initialized in the declaration of the corresponding field of an entity class, see for example active and grade fields in Customer.java.

Additionally, a specific initialization method with a @PostConstruct annotation can be created in the entity class. In this case, any global infrastructure interfaces and beans can be invoked during initialization, see for example the init() method in Customer.java.

4.2.1.2. Initialization Using CreateAction

If the initial value of an attribute depends on the data of the invoking screen, you can use setInitialValues() or setInitialValuesSupplier() methods of the CreateAction class.

See an example of handling Customer and CustomerAddress entities in the demo application:

init values 1
  • customer-address-browse.xml - a screen descriptor with two linked tables, one for customers and another for their addresses.

  • CustomerAddressBrowse.java - the screen controller. In its init() method, the setInitialValuesSupplier() is used to provide initial value for customer attribute of a created address. It will be the currently selected in the first table customer.

4.2.1.3. Using initNewItem Method

Initial values can also be defined in the initNewItem() method of the screen controller of the created entity.

Consider the following entities:

composition recipe 3

In the demo application, CustomerDetails attribute (info) is edited on the same screen as Customer itself. It requires creating of a CustomerDetails instance together with the owning Customer.

  • customer-edit.xml - a customer edit screen descriptor. It contains a nested datasource for a linked CustomerDetails instance. The infoField text area component is connected to this datasource.

  • CustomerEdit.java - the screen controller. It defines the initNewItem() method that creates a new CustomerDetails instance and sets it to a new Customer. The created instance will be available through the nested datasource and later saved to the database when the screen is committed.

4.2.2. Composite Structures

CUBA platform supports two types of relationship between entities: association and composition. They are called ASSOCIATION and COMPOSITION respectively in the CUBA Studio interface. Association is a relationship between the objects that can exist separately from each other. Composition, on the other hand, is used for "master-detail" relations, when the detail instances can exist only as part of the master. A case of an airport and its terminals may be considered an example of composition: a terminal that does not belong to any airport does not make sense.

Typically, the entities belonging to a composition are edited together since it is more natural. For example, a user opens the airport editing screen and sees the list of terminals, so the user can create and edit them, but all changes both for the airport and the terminals are saved to the database together in one transaction, and only after the user confirms saving of the master entity (the airport).

4.2.2.1. One-to-Many: One Level of Nesting

Let’s implement a one-to-many composition using the Airport and the Terminal entities as an example:

composition recipe 1
  • Terminal.java - the Terminal entity contains a mandatory link to the Airport.

    In the Studio entity designer, set for the airport attribute: Attribute type - ASSOCIATION, Cardinality - MANY_TO_ONE, Mandatory - on.

  • Airport.java - the Airport entity contains a one-to-many collection of terminals. The corresponding field is annotated with @Composition in order to implement composition, and @OnDelete for cascaded soft delete.

    In the Studio entity designer, set for the terminals attribute: Attribute type - COMPOSITION, Cardinality - ONE_TO_MANY, On delete - CASCADE.

  • views.xml - the airport-terminals view of the airport editing screen contains the terminals collection attribute. We are using the _local view for this attribute, because the airport attribute of the Terminal entity is set only at the creation of a new Terminal instance and never changes after that, so we do not need to load it.

  • airport-edit.xml - the XML descriptor of the airport editor defines a datasource for the Airport instance and a nested one for its terminals. It also contains a table displaying terminals.

  • terminal-edit.xml - a standard editor for the Terminal entity.

As a result, editing of an airport instance works as follows:

  • The airport edit screen shows a list of terminals.

  • A user can pick a terminal and open its editor. When OK is clicked in the terminal editor, the updated instance of the terminal is not saved to the database, but to the terminalsDs datasource of the airport editor.

  • The user can create new terminals and delete existing ones. All changes will be saved to the terminalsDs datasource.

  • When a user clicks OK in the airport edit screen, the updated Airport instance together with all the updated Terminal instances is submitted to the DataManager.commit() method on the middleware and saved to the database within a single transaction.

4.2.2.2. One-to-Many: Two Levels of Nesting

Composition can be deeper, with up to two nested levels. Let’s extend the previous example by adding a MeetingPoint entity describing a meeting point at an airport terminal:

composition recipe 2

The Terminal entity contains the meetingPoints attribute – a collection of the MeetingPoint instances. In order for all three entities to become a single composition and be edited together, the following should be done in addition to the steps described above:

  • Terminal.java - the meetingPoints attribute of the Terminal class is marked as @Composition and @OnDelete similarly to the terminals attribute of the Airport class.

  • views.xml - the terminal-meetingPoints-view view of the Terminal class contains the meetingPoints collection attribute. This view is used in the airport-terminals-meetingPoints-view view of the Airport entity.

  • airport-edit.xml - the Airport edit screen XML descriptor contains datasources for an instance of the Airport and nested entities for the entire composition (airportDs > terminalsDs > meetingPointsDs).

    Here, the meetingPointsDs datasource is not associated with any visual components, however it is needed for correct operation of joint editing of the composition.

  • terminal-edit.xml - the terminal edit screen XML descriptor contains a nested datasource and a corresponding table for the meetingPoints collection.

As a result, the updated instances of the MeetingPoint, as well as the Terminal instances, will be saved to the database only with the Airport instance in the same transaction.

4.2.2.3. One-to-Many: Three Levels of Nesting

Suppose that you need an additional entity that contains some details of the meeting point: Note. So the whole structure looks as follows: Airport > Terminal > Meeting Point > Note.

composition recipe 4

CUBA can handle compositions with up to 2 levels of nesting. Here we have 3 levels, so we should limit the depth either from the top or from the bottom. Below we consider two different approaches (from the user experience perspective) of excluding the airport from the composition. Both of them solve the same problem: as now terminals are saved to the database independently from the airport, you cannot save a terminal for a newly created airport which is not saved to the database yet.

  • In the first approach, the airport browser and editor look the same as above, but the editor has additional Save button to save a new airport without closing the screen. A user cannot create terminals until the new airport is saved.

    • airport-edit.xml contains a standalone datasource for terminals instead of the nested one. This standalone datasource is linked to the airport datasource and thus loads terminals for the edited airport. Besides, airport editor contains extendedEditWindowActions frame which allows a user to save airport without closing the screen.

    • AirportEdit.java - here in the postInit() method of the airport editor, we manage the enabled state of the terminal’s Create action and pass the current airport instance to initialize the airport attribute of a created terminal.

  • In the second approach, we have split the airport browser into two panels: one for the list of airports and another for the dependent list of terminals. That is the list of terminals is now outside of the airport editor. The terminal’s Create action is disabled until an airport is selected.

    • airport-browse.xml contains a standalone datasource for the list of terminals. It is linked to the airports datasource and thus loads terminals for a selected airport.

    • AirportBrowse.java - here in the init() method of the airport browse controller, we manage the enabled state of the terminal’s Create action and pass the currently selected airport instance to initialize the airport attribute of a created terminal.

4.2.2.4. One-to-One Composition

The one-to-one composition will be illustrated by the Customer and CustomerDetails entities:

composition recipe 3
  • Customer.java - the Customer entity contains an optional link to CustomerDetails annotated with @Composition.

  • CustomerDetails.java - the CustomerDetails entity.

  • customer-edit.xml - the customer edit screen descriptor. It contains a nested datasource for the CustomerDetails instance. In order to load the nested instance, the root datasource uses a view of the Customer entity that includes the details attribute. The field group in the customer edit screen just declares a field for the details attribute.

As a result, customer editing works as follows:

composition recipe oto 1
  • When the open action is invoked, a new instance of CustomerDetails is created and its edit screen is shown. When OK is clicked in the details editor, the details instance is not saved to the database, but to the detailsDs datasource of the customer edit screen.

  • The picker field displays the instance name of the details entity:

composition recipe oto 2
  • When a user clicks OK in the customer edit screen, the updated Customer instance together with the CustomerDetails instance is submitted to the DataManager.commit() method on the Middleware and saved to the database within a single transaction.

  • If the user invokes the clear action of the picker field, the CustomerDetails instance is deleted and the reference to it is cleared in the same transaction after the user commits the customer editor.

4.3. Loading and Saving Data

This section describes different ways of loading and saving data to the database.

4.3.1. DataManager vs. EntityManager

Both DataManager and EntityManager can be used for CRUD operations on entities. There are the following differences between these interfaces:

DataManager EntityManager

DataManager is available on both middle and client tiers.

EntityManager is available only on the middle tier.

DataManager is a singleton bean. It can be injected or obtained via AppBeans.get().

You should obtain a reference to EntityManager through the Persistence interface.

DataManager defines a few high-level methods for working with detached entities: load(), loadList(), reload(), commit().

EntityManager mostly resembles the standard javax.persistence.EntityManager.

DataManager in fact delegates to DataStore implementations, so the DataManager features listed below apply only to the most common case when you work with entities located in a relational database:

DataManager EntityManager

DataManager always starts new transactions internally.

You have to open a transaction before working with EntityManager.

DataManager loads partial entities according to views. There are a few exceptions, see details here.

EntityManager loads all local attributes. If a view is specified, it affects only reference attributes. See details here.

DataManager executes only JPQL queries. Besides, it has separate methods for loading entities: load(), loadList(); and scalar values and aggregates: loadValues().

EntityManager can run any JPQL or native (SQL) queries.

DataManager checks security restrictions when invoked on the client tier.

EntityManager does not impose security restrictions.

When you work with data on the client tier, you have only one option - DataManager. On the middleware, use EntityManager when you need to implement some atomic logic inside a transaction or if the EntityManager interface is better suited to the task. Otherwise, on the middleware you can use both.

If you need to overcome restrictions of DataManager when working on the client tier, create your own service and use EntityManager to work with data. In the service, you can check permissions using the Security interface and return data to the client in the form of persistent or non-persistent entities or arbitrary values.

4.4. Using REST API

This section contains REST API usage examples.

The detailed information about REST API methods is written according to Swagger specification and is available at address http://files.cuba-platform.com/swagger/6.6.

4.4.1. Getting an OAuth Token

An OAuth token is required for any REST API method (except when you are using an anonymous access). A token can be obtained by the POST request on the address:

http://localhost:8080/app/rest/v2/oauth/token

An access to this endpoint is protected with a basic authentication. REST API client identifier and password is used for basic authentication. Please note that these are not an application user login and password. REST API client id and password are defined in the application properties cuba.rest.client.id and cuba.rest.client.secret (the default values are client and secret). You must pass the client id and secret, separated by a single colon (":") character, within a base64 encoded string in the Authorization header.

The request type must be application/x-www-form-urlencoded, the encoding is UTF-8.

The requst must contain the following parameters:

  • grant_type - always password.

  • username - application user login.

  • password - application user password.

POST /oauth/token
Authorization: Basic Y2xpZW50OnNlY3JldA==
Content-Type: application/x-www-form-urlencoded

grant_type=password&username=smith&password=qwerty123

Method returns a JSON object:

{
  "access_token": "29bc6b45-83cd-4050-8c7a-2a8a60adf251",
  "token_type": "bearer",
  "expires_in": 43198,
  "scope": "rest-api"
}

A token value is in the access_token property.

4.4.2. Custom Authentication

Authentication mechanisms can provide access tokens by key, link, LDAP login and password, etc. REST API uses its own authentication mechanism that cannot be modified. In order to use custom authentication process, you need to create a REST controller and use its URL.

Let’s consider the custom authentication mechanism that enables getting an OAuth token by a promo code. In the following example we will use a sample application that contains the Coupon entity with code attribute. We will send this attribute’s value as an authentication parameter in GET request.

  1. Create a Coupon entity with the code attribute:

    @Column(name = "CODE", unique = true, length = 4)
    protected String code;
  2. Create a user with promo-user login on behalf of which the authentication will be performed.

  3. Create a new Spring configuration file with name rest-dispatcher-spring.xml under the root package (com.company.demo) of web module. The content of the file must be as follows:

    <beans xmlns="http://www.springframework.org/schema/beans"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd">
    
        <context:component-scan base-package="com.company.demo.web.rest"/>
    
    </beans>
  4. Include the file into the cuba.restSpringContextConfig application property in the modules/web/src/web-app.properties file:

    cuba.restSpringContextConfig = +com/company/demo/rest-dispatcher-spring.xml
  5. Create the rest package under the root package of web module and implement the custom Spring MVC controller in it. Use the OAuthTokenIssuer bean to generate and issue the REST API token for a user after the custom authentication:

    @RestController
    @RequestMapping("auth-code")
    public class AuthCodeController {
        @Inject
        private OAuthTokenIssuer oAuthTokenIssuer;
        @Inject
        private LoginService loginService;
        @Inject
        private Configuration configuration;
        @Inject
        private DataManager dataManager;
        @Inject
        private MessageTools messageTools;
    
        // here we check secret code and issue token using OAuthTokenIssuer
        @RequestMapping(method = RequestMethod.GET)
        public ResponseEntity get(@RequestParam("code") String authCode) {
            // obtain system session to be able to call middleware services
            WebAuthConfig webAuthConfig = configuration.getConfig(WebAuthConfig.class);
            UserSession systemSession;
            try {
                systemSession = loginService.getSystemSession(webAuthConfig.getTrustedClientPassword());
            } catch (LoginException e) {
                throw new RuntimeException("Error during system auth");
            }
    
            // set security context
            AppContext.setSecurityContext(new SecurityContext(systemSession));
            try {
                // find coupon with code
                LoadContext<Coupon> loadContext = LoadContext.create(Coupon.class)
                        .setQuery(LoadContext.createQuery("select c from demo$Coupon c where c.code = :code")
                                .setParameter("code", authCode));
    
                if (dataManager.load(loadContext) == null) {
                    // if coupon is not found - code is incorrect
                    return new ResponseEntity<>(new ErrorInfo("invalid_grant", "Bad credentials"), HttpStatus.BAD_REQUEST);
                }
    
                // generate token for "promo-user"
                OAuthTokenIssuer.OAuth2AccessTokenResult tokenResult =
                        oAuthTokenIssuer.issueToken("promo-user", messageTools.getDefaultLocale(), Collections.emptyMap());
                OAuth2AccessToken accessToken = tokenResult.getAccessToken();
    
                // set security HTTP headers to prevent browser caching of security token
                HttpHeaders headers = new HttpHeaders();
                headers.set(HttpHeaders.CACHE_CONTROL, "no-store");
                headers.set(HttpHeaders.PRAGMA, "no-cache");
                return new ResponseEntity<>(accessToken, headers, HttpStatus.OK);
            } finally {
                // clean up security context
                AppContext.setSecurityContext(null);
            }
        }
    
        // POJO for JSON error messages
        public static class ErrorInfo {
            private String error;
            private String error_description;
    
            public ErrorInfo(String error, String error_description) {
                this.error = error;
                this.error_description = error_description;
            }
    
            public String getError() {
                return error;
            }
    
            public String getError_description() {
                return error_description;
            }
        }
    }
  6. Exclude the rest package from scanning in web/core modules: the OAuthTokenIssuer bean is available only in REST API context, and scanning for it in the application context will cause an error.

    <context:component-scan base-package="com.company.demo">
        <context:exclude-filter type="regex" expression="com\.company\.demo\.web\.rest\..*"/>
    </context:component-scan>
  7. Now users will be able to obtain OAuth2 access code using GET HTTP request with the code parameter to

    http://localhost:8080/app/rest/auth-code?code=A325

    The result will be:

    {"access_token":"74202587-6c2b-4d74-bcf2-0d687ea85dca","token_type":"bearer","expires_in":43199,"scope":"rest-api"}

    The obtained access token should then be passed to REST API, as described in the documentation.

4.4.2.1. Social Login in REST API

The mechanism of social login can be used in REST API too. The complete sample application is available on GitHub and described in the Social Login section, below are the key points of getting an access token with a Facebook account.

  1. Create the restapi package under the root package of web module and implement the custom Spring MVC controller in it. This controller should contain two main methods: get() to get a ResponseEntity instance and login() to obtain an OAuth token.

    @RequestMapping(method = RequestMethod.GET)
    public ResponseEntity get() {
        String loginUrl = getAsPrivilegedUser(() ->
                facebookService.getLoginUrl(getAppUrl(), OAuth2ResponseType.CODE_TOKEN)
        );
    
        HttpHeaders headers = new HttpHeaders();
        headers.set(HttpHeaders.LOCATION, loginUrl);
        return new ResponseEntity<>(headers, HttpStatus.FOUND);
    }

    Here we check the Facebook code, obtain an access code and issue the access token using OAuthTokenIssuer:

    @RequestMapping(method = RequestMethod.POST, value = "login")
    public ResponseEntity<OAuth2AccessToken> login(@RequestParam("code") String code) {
        User user = getAsPrivilegedUser(() -> {
            FacebookUserData userData = facebookService.getUserData(getAppUrl(), code);
    
            return socialRegistrationService.findOrRegisterUser(
                userData.getId(), userData.getEmail(), userData.getName());
        });
    
        OAuth2AccessTokenResult tokenResult = oAuthTokenIssuer.issueToken(user.getLogin(),
                messageTools.getDefaultLocale(), Collections.emptyMap());
    
        HttpHeaders headers = new HttpHeaders();
        headers.set(HttpHeaders.CACHE_CONTROL, "no-store");
        headers.set(HttpHeaders.PRAGMA, "no-cache");
        return new ResponseEntity<>(tokenResult.getAccessToken(), headers, HttpStatus.OK);
    }
  2. Exclude the restapi package from scanning in web/core modules: the OAuthTokenIssuer bean is available only in REST API context, and scanning for it in the application context will cause an error.

    <context:component-scan base-package="com.company.demo">
        <context:exclude-filter type="regex" expression="com\.company\.demo\.restapi\..*"/>
    </context:component-scan>
  3. Create the facebook-login-demo.html file in the modules/web/web/VAADIN folder of your project. It will contain the JavaScript code running on HTML page:

    <html>
    <head>
        <title>Facebook login demo with REST-API</title>
        <script src="jquery-3.2.1.min.js"></script>
        <style type="text/css">
     #users { display: none; }
        </style>
    </head>
    <body>
    <h1>Facebook login demo with REST-API</h1>
    
    <script type="application/javascript"...>
    </script>
    
    <a id="fbLink" href="/app/rest/facebook">Login with Facebook</a>
    
    <div id="users">
        You are logged in!
    
        <h1>Users</h1>
    
        <div id="usersList">
        </div>
    </div>
    
    </body>
    </html>

    The following script will try to login with Facebook. Firstly, it will remove code parameters from URL, then it will pass the code to REST API to get an OAuth access token, and in case of successful authentication we will be able to load and save data as usual.

    var oauth2Token = null;
    
    function tryToLoginWithFacebook() {
        var urlHash = window.location.hash;
    
        if (urlHash && urlHash.indexOf('&code=') >= 0) {
            console.log("Try to login to CUBA REST-API!");
    
            var urlCode = urlHash.substring(urlHash.indexOf('&code=') + '&code='.length);
            console.log("Facebook code: " + urlCode);
    
            history.pushState("", document.title, window.location.pathname);
    
            $.post({
                url: '/app/rest/facebook/login',
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded'
                },
                dataType: 'json',
                data: {code: urlCode},
                success: function (data) {
                    oauth2Token = data.access_token;
    
                    loadUsers();
                }
            })
        }
    }
    
    function loadUsers() {
        $.get({
            url: '/app/rest/v2/entities/sec$User?view=_local',
            headers: {
                'Authorization': 'Bearer ' + oauth2Token,
                'Content-Type': 'application/x-www-form-urlencoded'
            },
            success: function (data) {
                $('#fbLink').hide();
                $('#users').show();
    
                $.each(data, function (i, user) {
                    $('#usersList').append("<li>" + user.name + " (" + user.email + ")</li>");
                });
            }
        });
    }
    
    tryToLoginWithFacebook();

    Another example or running a JavaScript code from CUBA applications you can find in the JavaScript Usage Example section.

4.4.3. Getting an Entity Instances List

Let’s suppose that the system has a sales$Order entity and we need to get a list of this entity instances. Besides, we need to get not all the records, but only 50 records, starting with the 100th one. A response must contain not only simple properties of the sales$Order entity but also an information about the order customer (a reference field named customer). Orders must be sorted by date.

A base URL for getting all instances of the sales$Order entity is as follows:

http://localhost:8080/app/rest/v2/entities/sales$Order

To implement all the conditions described above the following request parameters must be specified:

  • view - a view, that will be used for loading entities. In our case the order-edit-view contains a customer reference.

  • limit - a number of instances to be returned.

  • offset - a position of the first extracted record.

  • sort - an entity attribute name that will be used for sorting.

An OAuth token must be put in the Authorization header with the Bearer type:

Authorization: Bearer 29bc6b45-83cd-4050-8c7a-2a8a60adf251

As a result, we get the following GET request URL:

http://localhost:8080/app/rest/v2/entities/sales$Order?view=order-edit-view&limit=50&offset=100&sort=date

The response will be like this:

[
  {
    "_entityName": "sales$Order",
    "_instanceName": "00001",
    "id": "46322d73-2374-1d65-a5f2-160461da22bf",
    "date": "2016-10-31",
    "description": "Vacation order",
    "number": "00001",
    "items": [
      {
        "_entityName": "sales$OrderItem",
        "_instanceName": "Beach umbrella",
        "id": "95a04f46-af7a-a307-de4e-f2d73cfc74f7",
        "price": 23,
        "name": "Beach umbrella"
      },
      {
        "_entityName": "sales$OrderItem",
        "_instanceName": "Sun lotion",
        "id": "a2129675-d158-9e3a-5496-41bf1a315917",
        "price": 9.9,
        "name": "Sun lotion"
      }
    ],
    "customer": {
      "_entityName": "sales$Customer",
      "_instanceName": "Toby Burns",
      "id": "4aa9a9d8-01df-c8df-34c8-c385b566ea05",
      "firstName": "Toby",
      "lastName": "Burns"
    }
  },
  {
    "_entityName": "sales$Order",
    "_instanceName": "00002",
    "id": "b2ad3059-384c-3e03-b62d-b8c76621b4a8",
    "date": "2016-12-31",
    "description": "New Year party set",
    "number": "00002",
    "items": [
      {
        "_entityName": "sales$OrderItem",
        "_instanceName": "Jack Daniels",
        "id": "0c566c9d-7078-4567-a85b-c67a44f9d5fe",
        "price": 50.7,
        "name": "Jack Daniels"
      },
      {
        "_entityName": "sales$OrderItem",
        "_instanceName": "Hennessy X.O",
        "id": "c01be87b-3f91-7a86-50b5-30f2f0a49127",
        "price": 79.9,
        "name": "Hennessy X.O"
      }
    ],
    "customer": {
      "_entityName": "sales$Customer",
      "_instanceName": "Morgan Collins",
      "id": "5d111245-2ed0-abec-3bee-1a196da92e3e",
      "firstName": "Morgan",
      "lastName": "Collins"
    }
  }
]

Please note, that every entity in the response has a _entityName attribute with the entity name and an _instanceName attribute with the entity instance name.

4.4.4. New Entity Instance Creation

New sales$Order entity instance can be created with the POST request on the address:

http://localhost:8080/app/rest/v2/entities/sales$Order

An OAuth token must be put in the Authorization header with the Bearer type.

The request body must contain a JSON object that describes a new entity instance, e.g.:

{
  "number": "00017",
  "date": "2016-09-01",
  "description": "Back to school",
  "items": [
    {
      "_entityName": "sales$OrderItem",
      "price": 100,
      "name": "School bag"
    },
    {
      "_entityName": "sales$OrderItem",
      "price": 9.90,
      "name": "Pencils"
    }
  ],
  "customer": {
    "id": "4aa9a9d8-01df-c8df-34c8-c385b566ea05"
  }
}

A collection of order items (items) and a customer reference are passed in the request body. Let’s examine how these attributes will be processed.

First, let’s have a quick look to the Order class:

package com.company.sales.entity;

import com.haulmont.chile.core.annotations.Composition;
import com.haulmont.chile.core.annotations.NamePattern;
import com.haulmont.cuba.core.entity.StandardEntity;
import com.haulmont.cuba.core.entity.annotation.OnDelete;
import com.haulmont.cuba.core.global.DeletePolicy;

import javax.persistence.*;
import java.util.Date;
import java.util.Set;

@NamePattern("%s|number")
@Table(name = "SALES_ORDER")
@Entity(name = "sales$Order")
public class Order extends StandardEntity {
    private static final long serialVersionUID = 7565070704618724997L;

    @Column(name = "NUMBER_")
    protected String number;

    @Temporal(TemporalType.DATE)
    @Column(name = "DATE_")
    protected Date date;

    @Column(name = "DESCRIPTION")
    protected String description;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "CUSTOMER_ID")
    protected Customer customer;

    @Composition
    @OnDelete(DeletePolicy.CASCADE)
    @OneToMany(mappedBy = "order")
    protected Set<OrderItem> items;

    //getters and setters omitted
}

The items collection property is annotated with the @Composition. REST API methods for entity creation and update will create a new entity instances for all members of such collections. In our case, two instances of OrderItem entity will be created with the Order entity.

The customer reference doesn’t have a @Composition annotation, that’s why the REST API will try to find a client with the given id and set it to the customer field. If the client is not found then an order won’t be created and the method will return an error.

In case of successful method execution a full object graph of the created entity is returned:

{
  "_entityName": "sales$Order",
  "id": "5d7ff8e3-7828-ba94-d6ba-155c5c4f2a50",
  "date": "2016-09-01",
  "description": "Back to school",
  "version": 1,
  "number": "00017",
  "createdBy": "admin",
  "createTs": "2016-10-13 18:12:21.047",
  "updateTs": "2016-10-13 18:12:21.047",
  "items": [
    {
      "_entityName": "sales$OrderItem",
      "id": "3158b8ed-7b7a-568e-aec5-0822c3ebbc24",
      "createdBy": "admin",
      "price": 9.9,
      "name": "Pencils",
      "createTs": "2016-10-13 18:12:21.047",
      "version": 1,
      "updateTs": "2016-10-13 18:12:21.047",
      "order": {
        "_entityName": "sales$Order",
        "id": "5d7ff8e3-7828-ba94-d6ba-155c5c4f2a50"
      }
    },
    {
      "_entityName": "sales$OrderItem",
      "id": "72774b8b-4fea-6403-7b52-4a6a749215fc",
      "createdBy": "admin",
      "price": 100,
      "name": "School bag",
      "createTs": "2016-10-13 18:12:21.047",
      "version": 1,
      "updateTs": "2016-10-13 18:12:21.047",
      "order": {
        "_entityName": "sales$Order",
        "id": "5d7ff8e3-7828-ba94-d6ba-155c5c4f2a50"
      }
    }
  ],
  "customer": {
    "_entityName": "sales$Customer",
    "id": "4aa9a9d8-01df-c8df-34c8-c385b566ea05",
    "firstName": "Toby",
    "lastName": "Burns",
    "createdBy": "admin",
    "createTs": "2016-10-13 15:32:01.657",
    "version": 1,
    "updateTs": "2016-10-13 15:32:01.657"
  }
}

4.4.5. Existing Entity Instance Update

An existing sales$Order entity instance can be updated with the PUT request on the address:

http://localhost:8080/app/rest/v2/entities/sales$Order/5d7ff8e3-7828-ba94-d6ba-155c5c4f2a50

The last part of the query here is the entity identifier.

An OAuth token must be put in the Authorization header with the Bearer type.

The request body must contain a JSON object containing only fields we want to update, e.g.:

{
  "date": "2017-10-01",
  "customer" : {
    "id" : "5d111245-2ed0-abec-3bee-1a196da92e3e"
  }
}

The response body will contain a modified entity:

{
  "_entityName": "sales$Order",
  "id": "5d7ff8e3-7828-ba94-d6ba-155c5c4f2a50",
  "date": "2017-10-01",
  "updatedBy": "admin",
  "description": "Back to school",
  "version": 2,
  "number": "00017",
  "createdBy": "admin",
  "createTs": "2016-10-13 18:12:21.047",
  "updateTs": "2016-10-13 19:13:02.656",
  "customer": {
    "_entityName": "sales$Customer",
    "id": "5d111245-2ed0-abec-3bee-1a196da92e3e",
    "firstName": "Morgan",
    "lastName": "Collins",
    "createdBy": "admin",
    "createTs": "2016-10-13 15:31:27.821",
    "version": 1,
    "updateTs": "2016-10-13 15:31:27.821",
    "email": "collins@gmail.com"
  }
}

4.4.6. Executing a JPQL Query (GET)

Before the execution with the REST API a query must be described in the configuration file. The rest-queries.xml file must be created in the main package of the web module (e.g. com.company.sales). Then the file must be defined in the application properties file of the web module (web-app.properties).

cuba.rest.queriesConfig = +com/company/sales/rest-queries.xml

rest-queries.xml contents:

<?xml version="1.0"?>
<queries xmlns="http://schemas.haulmont.com/cuba/rest-queries.xsd">
    <query name="ordersAfterDate" entity="sales$Order" view="order-edit-view">
        <jpql><![CDATA[select o from sales$Order o where o.date >= :startDate and o.date <= :endDate]]></jpql>
        <params>
            <param name="startDate" type="java.util.Date"/>
            <param name="endDate" type="java.util.Date"/>
        </params>
    </query>
</queries>

To execute a JPQL query the following GET request must be executed:

http://localhost:8080/app/rest/v2/queries/sales$Order/ordersAfterDate?startDate=2016-11-01&endDate=2017-11-01

The request URL parts:

  • sales$Order - extracted entity name.

  • ordersAfterDate - a query name from the configuration file.

  • startDate and endDate - request parameters with the values.

An OAuth token must be put in the Authorization header with the Bearer type.

The method returns a JSON array of extracted entity instances:

[
  {
    "_entityName": "sales$Order",
    "_instanceName": "00002",
    "id": "b2ad3059-384c-3e03-b62d-b8c76621b4a8",
    "date": "2016-12-31",
    "description": "New Year party set",
    "number": "00002",
    "items": [
      {
        "_entityName": "sales$OrderItem",
        "_instanceName": "Jack Daniels",
        "id": "0c566c9d-7078-4567-a85b-c67a44f9d5fe",
        "price": 50.7,
        "name": "Jack Daniels"
      },
      {
        "_entityName": "sales$OrderItem",
        "_instanceName": "Hennessy X.O",
        "id": "c01be87b-3f91-7a86-50b5-30f2f0a49127",
        "price": 79.9,
        "name": "Hennessy X.O"
      }
    ],
    "customer": {
      "_entityName": "sales$Customer",
      "_instanceName": "Morgan Collins",
      "id": "5d111245-2ed0-abec-3bee-1a196da92e3e",
      "firstName": "Morgan",
      "lastName": "Collins"
    }
  }
]

A full list of possible request parameters is available in the Swagger documentation.

4.4.7. Executing a JPQL Query (POST)

It is also possible to execute a query with POST HTTP request. POST request can be used when you need to pass a collection as query parameter value. In this case, the type of the query parameter in REST queries configuration file must end with square brackets: java.lang.String[], java.util.UUID[], etc.

<?xml version="1.0"?>
<queries xmlns="http://schemas.haulmont.com/cuba/rest-queries.xsd">
    <query name="ordersByIds" entity="sales$Order" view="order-edit-view">
        <jpql><![CDATA[select o from sales$Order o where o.id in :ids and o.status = :status]]></jpql>
        <params>
            <param name="ids" type="java.util.UUID[]"/>
            <param name="status" type="java.lang.String"/>
        </params>
    </query>
</queries>

Query parameters values must be passed in the request body as JSON map:

{
  "ids": ["c273fca1-33c2-0229-2a0c-78bc6d09110a", "e6c04c18-c8a1-b741-7363-a2d58589d800", "d268a4e1-f316-a7c8-7a96-87ba06afbbbd"],
  "status": "ready"
}

The POST request URL:

http://localhost:8080/app/rest/v2/queries/sales$Order/ordersByIds?returnCount=true

4.4.8. Service Method Invocation (GET)

Suppose there is an OrderService service in the system. The implementation looks as follows:

package com.company.sales.service;

import com.haulmont.cuba.core.EntityManager;
import com.haulmont.cuba.core.Persistence;
import com.haulmont.cuba.core.Transaction;
import org.springframework.stereotype.Service;
import javax.inject.Inject;
import java.math.BigDecimal;

@Service(OrderService.NAME)
public class OrderServiceBean implements OrderService {

    @Inject
    private Persistence persistence;

    @Override
    public BigDecimal calculatePrice(String orderNumber) {
        BigDecimal orderPrice = null;
        try (Transaction tx = persistence.createTransaction()) {
            EntityManager em = persistence.getEntityManager();
            orderPrice = (BigDecimal) em.createQuery("select sum(oi.price) from sales$OrderItem oi where oi.order.number = :orderNumber")
                    .setParameter("orderNumber", orderNumber)
                    .getSingleResult();
            tx.commit();
        }

        return orderPrice;
    }
}

Before the execution with the REST API a service method invocation must be allowed in the configuration file. The rest-services.xml file must be created in the main package of the web module (e.g. com.company.sales). Then the file must be defined in the application properties file of the web module (web-app.properties).

cuba.rest.servicesConfig = +com/company/sales/rest-services.xml

rest-services.xml content:

<?xml version="1.0" encoding="UTF-8"?>
<services xmlns="http://schemas.haulmont.com/cuba/rest-services-v2.xsd">
    <service name="sales_OrderService">
        <method name="calculatePrice">
            <param name="orderNumber"/>
        </method>
    </service>
</services>

To invoke the service method the following GET request must be executed:

http://localhost:8080/app/rest/v2/services/sales_OrderService/calculatePrice?orderNumber=00001

The request URL parts:

  • sales_OrderService - a service name.

  • calculatePrice - a method name.

  • orderNumber - an argument name with the value.

An OAuth token must be put in the Authorization header with the Bearer type.

A service method may return a result of simple datatype, an entity, an entities collection or a serializable POJO. In our case a BigDecimal is returned, so the response body contains just a number:

39.2

4.4.9. Service Method Invocation (POST)

REST API allows execution not only of methods that have arguments of simple datatypes, but also of methods with the following arguments:

  • entities

  • entities collections

  • POJOs

Suppose we added a new method to the OrderService created in the previous section:

@Override
public OrderValidationResult validateOrder(Order order, Date validationDate){
    OrderValidationResult result=new OrderValidationResult();
    result.setSuccess(false);
    result.setErrorMessage("Validation of order "+order.getNumber()+" failed. validationDate parameter is: "+validationDate);
    return result;
}

OrderValidationResult class looks as follows:

package com.company.sales.service;

import java.io.Serializable;

public class OrderValidationResult implements Serializable {

    private boolean success;

    private String errorMessage;

    public boolean isSuccess() {
        return success;
    }

    public void setSuccess(boolean success) {
        this.success = success;
    }

    public String getErrorMessage() {
        return errorMessage;
    }

    public void setErrorMessage(String errorMessage) {
        this.errorMessage = errorMessage;
    }
}

The new method has an Order entity in the arguments list and returns a POJO.

Before the invocation with the REST API the method must be allowed, so we add a record to the rest-services.xml configuration file (it was described in the Service Method Invocation (GET)).

<?xml version="1.0" encoding="UTF-8"?>
<services xmlns="http://schemas.haulmont.com/cuba/rest-services-v2.xsd">
    <service name="sales_OrderService">
        <method name="calculatePrice">
            <param name="orderNumber"/>
        </method>
        <method name="validateOrder">
            <param name="order"/>
            <param name="validationDate"/>
        </method>
    </service>
</services>

The validateOrder service method may be called with the POST request on the address:

http://localhost:8080/app/rest/v2/services/sales_OrderService/validateOrder

In case of the POST request parameters are passed in the request body. The request body must contain a JSON object, each field of this object corresponds to the service method argument.

{
  "order" : {
    "number": "00050",
    "date" : "2016-01-01"
  },
  "validationDate": "2016-10-01"
}

An OAuth token must be put in the Authorization header with the Bearer type.

The REST API method returns a serialized POJO:

{
  "success": false,
  "errorMessage": "Validation of order 00050 failed. validationDate parameter is: 2016-10-01"
}

4.4.10. Files Downloading

When downloading a file, passing a security token in the request header is often inconvenient. It is desirable to have a URL for downloading that may be put to the src attribute of the img tag.

As a solution, an OAuth token can also be passed in the request URL as a parameter with the access_token name.

For example, an image is uploaded to the application. Its FileDescriptor id is 44809679-e81c-e5ae-dd81-f56f223761d6.

In this case a URL for downloading the image will look like this:

http://localhost:8080/app/rest/v2/files/44809679-e81c-e5ae-dd81-f56f223761d6?access_token=a2f0bb4e-773f-6b59-3450-3934cbf0a2d6

4.4.11. JavaScript Usage Example

This section contains an example of using REST API v2 from JavaScript running on a HTML page. The page initially shows login form, and after successful login displays a message and a list of entities.

For simplicity, we will use modules/web/web/VAADIN folder for storing HTML/CSS/JavaScript files, as the corresponding folder in the deployed web application is used for serving static resources by default. So you will not need to make any configuration of your Tomcat application server. The resulting URL will start from http://localhost:8080/app/VAADIN, so do not use this approach in a real world application - create a separate web application with its own context instead.

Download jQuery and Bootstrap and copy to modules/web/web/VAADIN folder of your project. Create customers.html and customers.js files, so the content of the folder should look as follows:

bootstrap.min.css
customers.html
customers.js
jquery-3.1.1.min.js

customers.html file content:

<html>
    <head>
        <script type="text/javascript" src="jquery-3.1.1.min.js"></script>
        <link rel="stylesheet" href="bootstrap.min.css"/>
    </head>
    <body>
        <div style="width: 300px; margin: auto;">
            <h1>Sales</h1>

            <div id="loggedInStatus" style="display: none" class="alert alert-success">
                Logged in successfully
            </div>
            <div id="loginForm">
                <div class="form-group">
                    <label for="loginField">Login:</label>
                    <input type="text" class="form-control" id="loginField">
                </div>
                <div class="form-group">
                    <label for="passwordField">Password:</label>
                    <input type="password" class="form-control" id="passwordField">
                </div>
                <button type="submit" class="btn btn-default" onclick="login()">Submit</button>
            </div>

            <div id="customers" style="display: none">
                <h2>Customers</h2>
                <ul id="customersList"></ul>
            </div>
        </div>
        <script type="text/javascript" src="customers.js"></script>
    </body>
</html>

customers.js file content:

var oauthToken = null;
function login() {
    var userLogin = $('#loginField').val();
    var userPassword = $('#passwordField').val();
    $.post({
        url: 'http://localhost:8080/app/rest/v2/oauth/token',
        headers: {
            'Authorization': 'Basic Y2xpZW50OnNlY3JldA==',
            'Content-Type': 'application/x-www-form-urlencoded'
        },
        dataType: 'json',
        data: {grant_type: 'password', username: userLogin, password: userPassword},
        success: function (data) {
            oauthToken = data.access_token;
            $('#loggedInStatus').show();
            $('#loginForm').hide();
            loadCustomers();
        }
    })
}

function loadCustomers() {
    $.get({
        url: 'http://localhost:8080/app/rest/v2/entities/sales$Customer?view=_local',
        headers: {
            'Authorization': 'Bearer ' + oauthToken,
            'Content-Type': 'application/x-www-form-urlencoded'
        },
        success: function (data) {
            $('#customers').show();
            $.each(data, function (i, customer) {
                $('#customersList').append("<li>" + customer.name + " (" + customer.email + ")</li>");
            });
        }
    });
}

Login and password from the user input are sent to the server by the POST request with the Base64-encoded client credentials in the Authorization header as explained in Getting an OAuth Token section. If the authentication is successful, the web page receives an access token value from the server, the token is stored in the oauthToken variable, the loginForm div is hidden and the loggedInStatus div is shown.

To show the list of customers, the request is sent to the server to get the instances of the sales$Customer entity, passing the oauthToken value in the Authorization header.

In case the request is processed successfully, the customers div is shown, and the customersList element is filled with items containing customer names and emails.

rest js 1
rest js 2

4.4.12. Getting Localized Messages

There are methods in the REST API for getting localized messages for entities, their properties and enums.

For example, to get a list of localized messages for the sec$User entity you have to execute the following GET request:

http://localhost:8080/app/rest/v2/messages/entities/sec$User

An OAuth token must be put in the Authorization header with the Bearer type.

You can explicitly specify the desired locale using the Accept-Language http header.

The response will be like this:

{
  "sec$User": "User",
  "sec$User.active": "Active",
  "sec$User.changePasswordAtNextLogon": "Change Password at Next Logon",
  "sec$User.createTs": "Created At",
  "sec$User.createdBy": "Created By",
  "sec$User.deleteTs": "Deleted At",
  "sec$User.deletedBy": "Deleted By",
  "sec$User.email": "Email",
  "sec$User.firstName": "First Name",
  "sec$User.group": "Group",
  "sec$User.id": "ID",
  "sec$User.ipMask": "Permitted IP Mask",
  "sec$User.language": "Language",
  "sec$User.lastName": "Last Name",
  "sec$User.login": "Login",
  "sec$User.loginLowerCase": "Login",
  "sec$User.middleName": "Middle Name",
  "sec$User.name": "Name",
  "sec$User.password": "Password",
  "sec$User.position": "Position",
  "sec$User.substitutions": "Substitutions",
  "sec$User.timeZone": "Time Zone",
  "sec$User.timeZoneAuto": "Autodetect Time Zone",
  "sec$User.updateTs": "Updated At",
  "sec$User.updatedBy": "Updated By",
  "sec$User.userRoles": "User Roles",
  "sec$User.version": "Version"
}

To get the localization for enum, use the following URL:

http://localhost:8080/app/rest/v2/messages/enums/com.haulmont.cuba.security.entity.RoleType

If you omit the entity name or enum name part in the URL, you’ll get the localization for all entities or enums.

4.4.13. Data Model Versioning Example

Entity attribute was renamed

Let’s suppose that the oldNumber attribute of the sales$Order entity was renamed to newNumber and date was renamed to deliveryDate. In this case transformation config will be like this:

<?xml version="1.0"?>
<transformations xmlns="http://schemas.haulmont.com/cuba/rest-json-transformations.xsd">
    <transformation modelVersion="1.0" currentEntityName="sales$Order">
        <renameAttribute oldName="oldNumber" currentName="newNumber"/>
        <renameAttribute oldName="date" currentName="deliveryDate"/>
    </transformation>
    ...
</transformations>

If the client app needs to work with the old version of the sales$Order entity then it must pass the modelVersion value in the URL parameter:

http://localhost:8080/app/rest/v2/entities/sales$Order/c838be0a-96d0-4ef4-a7c0-dff348347f93?modelVersion=1.0

The following result will be returned:

{
  "_entityName": "sales$Order",
  "_instanceName": "00001",
  "id": "46322d73-2374-1d65-a5f2-160461da22bf",
  "date": "2016-10-31",
  "description": "Vacation order",
  "oldNumber": "00001"
}

The response JSON contains an oldNumber and date attributes although the entity in the CUBA application has newNumber and deliveryDate attributes.

Entity name was changed

Next, let’s imagine, that in some next release of the application a name of the sales$Order entity was also changed. The new name is sales$NewOrder.

Transformation config for version 1.1 will be like this:

<?xml version="1.0"?>
<transformations xmlns="http://schemas.haulmont.com/cuba/rest-json-transformations.xsd">
    <transformation modelVersion="1.1" oldEntityName="sales$Order" currentEntityName="sales$NewOrder">
        <renameAttribute oldName="oldNumber" currentName="newNumber"/>
    </transformation>
    ...
</transformations>

In addition to the config from the previous example an oldEntityName attribute is added here. It specifies the entity name that was valid for model version 1.1. The currentEntityName attribute specifies the current entity name.

Although an entity with a name sales$Order doesn’t exist anymore, the following request will work:

http://localhost:8080/app/rest/v2/entities/sales$Order/c838be0a-96d0-4ef4-a7c0-dff348347f93?modelVersion=1.1

The REST API controller will understand that it must search among sales$NewOrder entities and after the entity with given id is found names of the entity and of the newNumber attribute will be replaced in the result JSON:

{
  "_entityName": "sales$Order",
  "_instanceName": "00001",
  "id": "46322d73-2374-1d65-a5f2-160461da22bf",
  "date": "2016-10-31",
  "description": "Vacation order",
  "oldNumber": "00001"
}

The client app can also use the old version of data model for entity update and creation.

This POST request that uses old entity name and has old JSON in the request body will work:

http://localhost:8080/app/rest/v2/entities/sales$Order
{
  "_entityName": "sales$Order",
  "_instanceName": "00001",
  "id": "46322d73-2374-1d65-a5f2-160461da22bf",
  "date": "2016-10-31",
  "description": "Vacation order",
  "oldNumber": "00001"
}
Entity attribute must be removed from JSON

If some attribute was added to the entity, but the client that works with the old version of data model doesn’t expect this new attribute, then the new attribute can be removed from the result JSON.

Transformation configuration for this case will look like this:

<?xml version="1.0"?>
<transformations xmlns="http://schemas.haulmont.com/cuba/rest-json-transformations.xsd">
    <transformation modelVersion="1.5" currentEntityName="sales$Order">
        <toVersion>
            <removeAttribute name="discount"/>
        </toVersion>
    </transformation>
    ...
</transformations>

Transformation in this config file contains a toVersion tag with a nested removeAttribute command. This means that when the transformation from the current state to specific version is performed (i.e. when you request a list of entities) then a discount attribute must be removed from the result JSON.

In this case if you perform the request without the modelVersion attribute, the discount attribute will be returned:

http://localhost:8080/app/rest/v2/entities/sales$Order/c838be0a-96d0-4ef4-a7c0-dff348347f93
{
    "_entityName": "sales$Order",
    "_instanceName": "00001",
    "id": "46322d73-2374-1d65-a5f2-160461da22bf",
    "deliveryDate": "2016-10-31",
    "description": "Vacation order",
    "number": "00001",
    "discount": 50
}

If you specify the modelVersion then discount attribute will be removed

http://localhost:8080/app/rest/v2/entities/sales$Order/c838be0a-96d0-4ef4-a7c0-dff348347f93?modelVersion=1.1
{
    "_entityName": "sales$Order",
    "_instanceName": "00001",
    "id": "46322d73-2374-1d65-a5f2-160461da22bf",
    "deliveryDate": "2016-10-31",
    "description": "Vacation order",
    "oldNumber": "00001"
}
Using custom transformer

You can also create and register a custom JSON transformer. As an example let’s examine the following situation: there was an entity sales$OldOrder that was renamed to sales$NewOrder. This entity has an orderDate field. In the previous version, this date field contained a time part, but in the latest version of the entity, the time part is removed. REST API client that request the entity with an old model version 1.0 expects the date field to have the time part, so the transformer must modify the value in the JSON.

First, that’s how the transformer configuration must look like:

<?xml version="1.0"?>
<transformations xmlns="http://schemas.haulmont.com/cuba/rest-json-transformations.xsd">

    <transformation modelVersion="1.0" oldEntityName="sales$OldOrder" currentEntityName="sales$NewOrder">
        <custom>
            <fromVersion transformerBeanRef="sales_OrderJsonTransformerFromVersion"/>
            <toVersion transformerBeanRef="sales_OrderJsonTransformerToVersion"/>
        </custom>
    </transformation>

    ...
</transformations>

There are a custom element and nested toVersion and fromVersion elements. These elements have a reference to the transformer bean. This means that custom transformer must be registered as a Spring bean. There is one important thing here: a custom transformer may use the RestTransformations platform bean (this bean gives an access to other entities transformers if it is required). But the RestTransformations bean is registered in the Spring context of the REST API servlet, not in the main context of the web application. This means that custom transformer beans must be registered in the REST API Spring context as well.

That’s how we can do that.

First, create a rest-dispatcher-spring.xml in the web or portal module (e.g. in package com.company.test).

Next, register this file in the app.properties of the web or portal module:

cuba.restSpringContextConfig = +com/company/test/rest-dispatcher-spring.xml

The rest-dispatcher-spring.xml must contain custom transformer bean definitions:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd">

    <bean name="sales_OrderJsonTransformerFromVersion" class="com.company.test.transformer.OrderJsonTransformerFromVersion"/>
    <bean name="sales_OrderJsonTransformerToVersion" class="com.company.test.transformer.OrderJsonTransformerToVersion"/>

</beans>

The content of the sales_OrderJsonTransformerToVersion transformer is as follows:

package com.company.test.transformer;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Strings;
import com.haulmont.restapi.transform.AbstractEntityJsonTransformer;
import com.haulmont.restapi.transform.JsonTransformationDirection;

public class OrderJsonTransformerToVersion extends AbstractEntityJsonTransformer {

    public RepairJsonTransformerToVersion() {
        super("sales$NewOrder", "sales$OldOrder", "1.0", JsonTransformationDirection.TO_VERSION);
    }

    @Override
    protected void doCustomTransformations(ObjectNode rootObjectNode, ObjectMapper objectMapper) {
        JsonNode orderDateNode = rootObjectNode.get("orderDate");
        if (orderDateNode != null) {
            String orderDateNodeValue = orderDateNode.asText();
            if (!Strings.isNullOrEmpty(orderDateNodeValue))
                rootObjectNode.put("orderDate", orderDateNodeValue + " 00:00:00.000");
        }
    }
}

This transformer finds the orderDate node in the JSON object and modifies its value by adding the time part to the value.

When the sales$OldOrder entity with a data model version 1.0 is requested, the result JSON will contain entities with orderDate fields that contain time part, although it is not stored in the database anymore.

A couple more words about custom transformers. They must implement the EntityJsonTransformer interface. You can also extend the AbstractEntityJsonTransformer class and override its doCustomTransformations method. The AbstractEntityJsonTransformer contains all functionality of the standard transformer.

4.5. Miscellaneous

Here you can find various recipes that do not belong to the above categories.

4.5.1. Getting Localized Messages

This section covers ways of getting localized messages in different parts of the application.

  • In screen XML-descriptors, component attributes for displaying static text (such as caption) can address localized messages using the rules of MessageTools.loadString() method. For example:

    • caption="msg://roleName" – gets a message defined by the roleName key in the message pack of the current screen. Screen message pack is defined by the messagesPack attribute of the root window element.

    • caption="msg://com.company.sample.entity/Role.name" – gets a message defined by the Role.name key in the com.company.sample.entity message pack.

  • In screen controllers, localized strings can be retrieved in the following ways:

    • From the current screen message pack:

      • Using getMessage() method inherited from the AbstractFrame base class. For example:

        String msg = getMessage("warningMessage");
      • Using formatMessage() method inherited from the AbstractFrame base class. In this case, the extracted message is used to format submitted parameters according to the rules of String.format() method. For example:

        messages.properties:

        warningMessage = Invalid email address: '%s'

        Java controller:

        String msg = formatMessage("warningMessage", email);
    • From an arbitrary messages pack via an injection of Messages infrastructure interface. For example:

      @Inject
      private Messages messages;
      
      @Override
      public void init(Map<String, Object> params) {
          String msg = messages.getMessage(getClass(), "warningMessage");
          ...
      }
  • For components managed by a Spring container (managed beans, services, JMX-beans, Spring MVC controllers of the portal module), localized messages can be retrieved with the help of the Messages infrastructure interface injection:

    @Inject
    protected Messages messages;
    ...
    String msg = messages.getMessage(getClass(), "warningMessage");
  • In application code where injection is not possible, the Messages interface can be obtained using the static get() method of the AppBeans class:

    protected Messages messages = AppBeans.get(Messages.class);
    ...
    String msg = messages.getMessage(getClass(), "warningMessage");

4.5.2. Loading and Displaying Images

Let’s consider a task of loading, storing and displaying employee photos:

  • An employee is represented by Employee entity.

  • Image files are stored in the FileStorage. The Employee entity contains a link to the corresponding FileDescriptor.

  • The Employee edit screen shows the picture and also supports uploading, downloading and clearing the picture.

Entity class with a link to the image file:

@Table(name = "SAMPLE_EMPLOYEE")
@Entity(name = "sample$Employee")
public class Employee extends StandardEntity {
...
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "IMAGE_FILE_ID")
    protected FileDescriptor imageFile;

    public void setImageFile(FileDescriptor imageFile) {
        this.imageFile = imageFile;
    }

    public FileDescriptor getImageFile() {
        return imageFile;
    }
}

A view for loading an Employee together with FileDescriptor should include all local attributes of FileDescriptor:

<view class="com.company.sample.entity.Employee"
      name="employee-edit">
    <property name="name"/>
    ...
    <property name="imageFile"
              view="_local">
    </property>
</view>

A fragment of the Employee edit screen XML descriptor:

<groupBox caption="Photo" spacing="true"
          height="250px" width="250px" expand="image">
    <image id="image"
           width="100%"
           align="MIDDLE_CENTER"
           scaleMode="CONTAIN"/>
    <hbox align="BOTTOM_LEFT"
          spacing="true">
        <upload id="uploadField"/>
        <button id="downloadImageBtn"
                caption="Download"
                invoke="onDownloadImageBtnClick"/>
        <button id="clearImageBtn"
                caption="Clear"
                invoke="onClearImageBtnClick"/>
    </hbox>
</groupBox>

Components used to display, upload and download images are contained within the groupBox container. Its top part shows a picture using the image component, while its bottom part from left to right contains the upload component and buttons to download and clear the image. As a result, this part of the screen should look like this:

images recipe

Now, let us have a look at the edit screen controller.

import com.haulmont.cuba.core.entity.FileDescriptor;
import com.haulmont.cuba.core.global.FileStorageException;
import com.haulmont.cuba.gui.components.*;
import com.company.employeeimages.entity.Employee;
import com.haulmont.cuba.gui.data.DataSupplier;
import com.haulmont.cuba.gui.data.Datasource;
import com.haulmont.cuba.gui.export.ExportDisplay;
import com.haulmont.cuba.gui.export.ExportFormat;
import com.haulmont.cuba.gui.upload.FileUploadingAPI;

import javax.inject.Inject;
import java.util.Map;

public class EmployeeEdit extends AbstractEditor<Employee> {

    @Inject
    private DataSupplier dataSupplier;
    @Inject
    private FileUploadingAPI fileUploadingAPI;
    @Inject
    private ExportDisplay exportDisplay;
    @Inject
    private FileUploadField uploadField;
    @Inject
    private Button downloadImageBtn;
    @Inject
    private Button clearImageBtn;
    @Inject
    private Datasource<Employee> employeeDs;

    @Inject
    private Image image;

    @Override
    public void init(Map<String, Object> params) {
        uploadField.addFileUploadSucceedListener(event -> {
            FileDescriptor fd = uploadField.getFileDescriptor();
            try {
                fileUploadingAPI.putFileIntoStorage(uploadField.getFileId(), fd);
            } catch (FileStorageException e) {
                throw new RuntimeException("Error saving file to FileStorage", e);
            }
            getItem().setImageFile(dataSupplier.commit(fd));
            displayImage();
        });

        uploadField.addFileUploadErrorListener(event ->
                showNotification("File upload error", NotificationType.HUMANIZED));

        employeeDs.addItemPropertyChangeListener(event -> {
            if ("imageFile".equals(event.getProperty()))
                updateImageButtons(event.getValue() != null);
        });
    }

    @Override
    protected void postInit() {
        displayImage();
        updateImageButtons(getItem().getImageFile() != null);
    }

    public void onDownloadImageBtnClick() {
        if (getItem().getImageFile() != null)
            exportDisplay.show(getItem().getImageFile(), ExportFormat.OCTET_STREAM);
    }

    public void onClearImageBtnClick() {
        getItem().setImageFile(null);
        displayImage();
    }

    private void updateImageButtons(boolean enable) {
        downloadImageBtn.setEnabled(enable);
        clearImageBtn.setEnabled(enable);
    }

    private void displayImage() {
        if (getItem().getImageFile() != null) {
            image.setSource(FileDescriptorResource.class).setFileDescriptor(getItem().getImageFile());
            image.setVisible(true);
        } else {
            image.setVisible(false);
        }
    }
}
  • The init() method first initializes the uploadField component that is used for uploading new images. In the case of a successful upload, a new FileDescriptor instance is retrieved from the component and the corresponding files are sent from the temporary client storage to FileStorage by invoking FileUploadingAPI.putFileIntoStorage(). After that, the FileDescriptor is saved to the database by invoking DataSupplier.commit(), and the saved instance is assigned to the imageFile attribute of the edited Employee entity. Then, the controller’s displayImage() method is invoked to display the uploaded image.

    After that, a listener is added in the init() method to the datasource containing an Employee instance. The listener enables or disables download and clear buttons, depending on the fact whether the file has been loaded or not.

  • postInit() method performs file display and refreshes the button states, depending on the existence of a loaded file.

  • onDownloadImageBtnClick() is invoked when the downloadImageBtn button is clicked; it downloads the file using the ExportDisplay interface.

  • onClearImageBtnClick() is invoked when the clearImageBtn is clicked; it clears the imageFile attribute of the Employee entity. The file is not deleted from storage.

  • displayImage() loads the file from storage and sets the content of the image component.

4.5.2.1. Displaying Images in a Table Column

To amplify the previous task, let’s add pictures to the table as employees' icons on the Employee browse screen.

The pictures can be displayed in a separate column or inside any existing column. In both cases the Table.ColumnGenerator interface is used.

Below is a fragment of the Employee browse screen XML descriptor:

<groupTable id="employeesTable"
            width="100%">
    <actions>
        <action id="create"/>
        <action id="edit"/>
        <action id="remove"/>
    </actions>
    <columns>
        <column id="name"/>
    </columns>
    <rows datasource="employeesDs"/>
    <rowsCount/>
    <buttonsPanel id="buttonsPanel"
                  alwaysVisible="true">
        <button id="createBtn"
                action="employeesTable.create"/>
        <button id="editBtn"
                action="employeesTable.edit"/>
        <button id="removeBtn"
                action="employeesTable.remove"/>
    </buttonsPanel>
</groupTable>

To display pictures inline with an employee’s name in the name column, let’s change the standard representation of data in this column. We will use the HBoxLayout container and place the Image component into it:

import com.haulmont.cuba.core.entity.FileDescriptor;
import com.haulmont.cuba.gui.components.*;
import com.company.employeeimages.entity.Employee;
import com.haulmont.cuba.gui.xml.layout.ComponentsFactory;

import javax.inject.Inject;
import java.util.Map;

import static com.haulmont.cuba.gui.components.Image.*;

public class EmployeeBrowse extends AbstractLookup {

    @Inject
    private ComponentsFactory componentsFactory;

    @Inject
    private GroupTable<Employee> employeesTable;

    @Override
    public void init(Map<String, Object> params) {

        employeesTable.addGeneratedColumn("name", entity -> {
            Image image = componentsFactory.createComponent(Image.class);
            image.setScaleMode(ScaleMode.CONTAIN);
            image.setHeight("40");
            image.setWidth("40");

            FileDescriptor userImageFile = entity.getImageFile();
            image.setSource(FileDescriptorResource.class).setFileDescriptor(userImageFile);

            Label userLogin = componentsFactory.createComponent(Label.class);
            userLogin.setValue(entity.getName());
            userLogin.setAlignment(Alignment.MIDDLE_LEFT);

            HBoxLayout hBox = componentsFactory.createComponent(HBoxLayout.class);
            hBox.setSpacing(true);
            hBox.add(image);
            hBox.add(userLogin);

            return hBox;
        });
    }
}
  • The init() method invokes the addGeneratedColumn() method that takes two parameters: an identifier of the column and an implementation of the Table.ColumnGenerator interface. The latter is used to define the custom representation of data in the name column.

  • Inside this method we create an Image component using the ComponentsFactory interface. We set the scale mode of the component (CONTAIN) and its size parameters.

  • Then we get the FileDescriptor instance with the picture stored in the File Storage. The link to this picture is stored in the imageFile attribute of the Employee entity. The FileDescriptorImageResource resource type is used to set the source for the Image component.

  • We will display the name attribute in the Label component alongside the picture.

  • We will wrap both Image and Label components into the HBoxLayout container, and make the addGeneratedColumn() method return this container as the new table cell layout.

image recipe

You can also use a more declarative approach with the generator XML attribute.

4.5.3. Sending Emails

This section contains a practical guide to sending emails using the CUBA email sending mechanism.

Let us consider the following task:

  • There are the NewsItem entity and the NewsItemEdit screen.

  • The NewsItem entity contains the following attributes: date, caption, content.

  • We want to send emails to some addresses every time a new instance of NewsItem is created through the NewsItemEdit screen. An email should contain NewsItem.caption as a subject and the message body should be created from a template including NewsItem.content.

  1. Add the following code to NewsItemEdit.java:

    public class NewsItemEdit extends AbstractEditor<NewsItem> {
    
        // Indicates that a new item was created in this editor
        private boolean justCreated;
    
        @Inject
        protected EmailService emailService;
    
        // This method is invoked when a new item is initialized
        @Override
        protected void initNewItem(NewsItem item) {
            justCreated = true;
        }
    
        // This method is invoked after the screen commit
        @Override
        protected boolean postCommit(boolean committed, boolean close) {
            if (committed && justCreated) {
                // If a new entity was saved to the database, ask a user about sending an email
                showOptionDialog(
                        "Email",
                        "Send the news item by email?",
                        MessageType.CONFIRMATION,
                        new Action[] {
                                new DialogAction(DialogAction.Type.YES) {
                                    @Override
                                    public void actionPerform(Component component) {
                                        sendByEmail();
                                    }
                                },
                                new DialogAction(DialogAction.Type.NO)
                        }
                );
            }
            return super.postCommit(committed, close);
        }
    
        // Queues an email for sending asynchronously
        private void sendByEmail() {
            NewsItem newsItem = getItem();
            EmailInfo emailInfo = new EmailInfo(
                    "john.doe@company.com,jane.roe@company.com", // recipients
                    newsItem.getCaption(), // subject
                    null, // the "from" address will be taken from the "cuba.email.fromAddress" app property
                    "com/company/demo/templates/news_item.txt", // body template
                    Collections.singletonMap("newsItem", newsItem) // template parameters
            );
            emailService.sendEmailAsync(emailInfo);
        }
    }

    As you can see, the sendByEmail() method invokes the EmailService and passes the EmailInfo instance describing the the messages. The body of the messages will be created on the basis of the news_item.txt template.

  2. Create the body template file news_item.txt in the com.company.demo.templates package of the core module:

    The company news:
    ${newsItem.content}

    This is a Freemarker template which will use parameters passed in the EmailInfo instance (newsItem in this case).

  3. Launch the application, open the NewsItem entity browser and click Create. The editor screen will be opened. Fill in the fields and press OK. The confirmation dialog with the question about sending emails will be shown. Click Yes.

  4. Go to the Administration > Email History screen of your application. You will see two records (by the number of recipients) with the Queue status. It means that the emails are in the queue and not yet sent.

  5. To process the queue, set up a scheduled task. Go to the Administration > Scheduled Tasks screen of your application. Create a new task and set the following parameters:

    • Bean Name - cuba_Emailer

    • Method Name - processQueuedEmails()

    • Singleton - yes (this is important only for a cluster of middleware servers)

    • Period, sec - 10

    Save the task and click Activate on it.

    If you did not set up the scheduled tasks execution for this project before, nothing will happen on this stage - the task will not be executed until you start the whole scheduling mechanism.

  6. Open the modules/core/src/app.properties file and add the following property:

    cuba.schedulingActive = true

    Restart the application server. The scheduling mechanism is now active and invokes the email queue processing.

  7. Go to the Administration > Email History screen. The status of the emails will be Sent if they were successfully sent, or, most probably, Sending or Queue otherwise. In the latter case, you can open the application log in build/tomcat/logs/app.log and find out the reason. The email sending mechanism will take several (10 by default) attempts to send the messages and if they fail, set the status to Not sent.

  8. The most obvious reason that emails cannot be sent is that you have not set up the SMTP server parameters. You can set the parameters in the database through the app-core.cuba:type=Emailer JMX bean or in the application properties file of your middleware. Let us consider the latter. Open the modules/core/src/app.properties file and add the required parameters:

    cuba.email.fromAddress = do-not-reply@company.com
    cuba.email.smtpHost = mail.company.com

    Restart the application server. Go to Administration > JMX Console, find the Emailer JMX bean and try to send a test email to yourself using the sendTestEmail() operation.

  9. Now your sending mechanism is set up correctly, but it will not send the messages in the Not sent state. So you have to create another NewsItem in the editor screen. Do it and then watch how the status of new messages in the Email History screen will change to Sent.

4.5.4. Using Application Components

Any CUBA application can be used as a component of another application. An application component is a full-stack library providing functionality on all layers - from database schema to business logic and UI.

In this section, we’ll consider an example of creating an application component and using it in a project. The component will provide a "Customer Management" functionality and include the Customer entity and corresponding UI screens. The application will use the Customer entity from the component as a reference in its Order entity.

app components sample
Creating the Customer Management component
  1. Create a new project in Studio and specify the following properties on the New project screen:

    • Project name - customers

    • Project namespace - cust

    • Root package - com.company.customers

  2. Edit Project properties and on the Advanced tab, set the Module prefix to cust. This is necessary to assemble component artifacts with names different from the default app.

  3. Create the Customer entity with at least the name attribute. Switch to the Instance name tab and specify add name to the name pattern attributes.

    Warning

    If your component contains @MappedSuperclass persistent classes, make sure they have descendants which are entities (i.e. annotated with @Entity) in the same project. Otherwise such base classes will not be properly enhanced and you will not be able to use them in applications.

  4. Generate DB scripts and create standard screens for the Customer entity: cust$Customer.browse and cust$Customer.edit. After that, go to main menu designer and rename the application menu item to customerManagement.

  5. Click to the App component descriptor link on the Project properties panel. Save the generated descriptor by clicking OK.

  6. Test the Customer Management functionality: Run > Create database, Run > Start application server, then open http://localhost:8080/cust in your web browser.

  7. Install the application component into the local Maven repository by executing the Run > Install app component menu command. This command just runs the install Gradle task after stopping Gradle daemons.

Creating the Sales application
  1. Create a new project in Studio and specify the following properties on the New project screen:

    • Project name - sales

    • Project namespace - sales

    • Root package - com.company.sales

  2. Edit Project properties and on the App components panel click the plus button next to Custom components. In the Custom application component dialog, select the customers project in the Registered project drop-down list. The list contains all project registered in Studio that have an app-component.xml descriptor. Click OK in the dialog. The Maven coordinates of the Customer Management component will appear in the list of custom components. Save the project properties by clicking OK.

  3. Create the Order entity and add the date and amount attributes. Then add the customer attribute as a many-to-one association with the Customer entity - it should be available in the Type drop-down list.

  4. Generate DB scripts and create standard screens for the Order entity. When creating standard screens, create a order-with-customer-view view that includes the customer attribute and use it for the screens.

  5. Test the application functionality: Run > Create database, Run > Start application server, then open http://localhost:8080/app in your web browser. The application will contain two top level menu items: Customer Management and Application with the corresponding functionality.

Modifying the Customer Management component

Suppose we have to change the component functionality (add an attribute to Customer) and then reassemble the application to incorporate the changes.

  1. Open the customers project in Studio.

  2. Edit the Customer entity and add the address attribute. When saving the entity, select both browser and editor screens to include the new attribute.

  3. Generate DB scripts - a script for altering table will be created. Save the scripts.

  4. Test the changes in the component: Run > Update database, Run > Start application server, then open http://localhost:8080/cust in your web browser.

  5. Re-install the application component into the local Maven repository by executing the Run > Install app component menu command.

  6. Close the customers project and open sales.

  7. Execute Build > Clean, then Build > Assemble project menu commands.

  8. Execute Run > Update database - the update script from the Customer Management component will be executed.

  9. Execute Run > Start application server and open http://localhost:8080/app in your web browser - the application will contain the Customer entity and screens with the new address attribute.

Sharing the Customer Management component

You can share the application component by uploading it to a remote Maven repository.

  1. Stop the Studio server.

  2. Set up a repository as explained in Setting Up a Private Artifact Repository.

  3. Open build.gradle of the customers project in a text editor. Replace the repository and its credentials in buildscript/repositories section and add the uploadRepository to the cuba section:

    buildscript {
        ...
        repositories {
            maven {
                url 'http://repo.company.com/nexus/content/groups/work' // repository containing CUBA and your own artifacts
                credentials {
                    username(rootProject.hasProperty('repoUser') ? rootProject['repoUser'] : 'admin')
                    password(rootProject.hasProperty('repoPass') ? rootProject['repoPass'] : 'admin123')
                }
            }
    ...
    cuba {
        ...
        uploadRepository {
            url = 'http://repo.company.com/nexus/content/repositories/snapshots' // repository for uploading your artifacts
            user = 'admin'
            password = 'admin123'
        }
    }
  4. Open the command line and run gradle assemble in the customers project root directory. This ensures your new repository caches CUBA artifacts required for working in Studio.

  5. In the Studio server window, specify your repository and credentials instead of the standard CUBA repository. Start the Studio server.

  6. Open the customers project in Studio.

  7. In the Studio Search dialog (Alt-/), find the uploadArchives Gradle task and run it. You can also run this task from the command line. The Customer Management component artifacts will be uploaded to your repository.

  8. Remove the component artifacts from your local Maven repository to ensure that they will be downloaded from the remote repository when the sales application is assembled the next time: just delete the .m2/repository/com/company folder located in your user home directory.

  9. Open the customers project in Studio. The repository URL in its build.gradle will be automatically changed to the one specified in the Studio server window.

  10. Now you can assemble and run the application - the Customer Management component will be downloaded from the remote repository.

4.5.5. Creating Custom Visual Components

As explained in the Custom Visual Components section, the standard set of visual components can be extended in your project. You have the following options:

  1. Integrate a Vaadin add-on. Many third-party Vaadin components are distributed as add-ons and available at https://vaadin.com/directory.

  2. Integrate a JavaScript component. You can create a Vaadin component using a JavaScript library.

  3. Create a new Vaadin component with the client part written on GWT.

Futher on, you can integrate the resulting Vaadin component into CUBA Generic UI to be able to use it declaratively in screen XML descriptors and bind to datasources.

And the final step of integration is the support of the new component in the Studio WYSIWYG layout editor.

This section gives you examples of creating new visual components with all the methods described above. Integration to the Generic UI and support in Studio are the same for all methods, so these topics are described only for a new component created on the basis of a Vaadin add-on.

4.5.5.1. Using a Third-party Vaadin Component

This is an example of using the Stepper component available at http://vaadin.com/addon/stepper, in an application project. The component enables changing text field value in steps using the keyboard, mouse scroll or built-in up/down buttons.

Create a new project in CUBA Studio and name it addon-demo.

A Vaadin add-on may be integrated if the application project has a web-toolkit module. Create the module by clicking the Create web toolkit module link of the Project properties navigator section.

Then click the New UI component link. The UI component generation page will open. Select the Vaadin add-on value in the Component type section.

studio vaadin addon wizard no gui

Fill in the following fields:

  • The Add-on Maven dependency field contains Maven coordinates of the Vaadin add-on. The add-on will be included as a dependency to the project. You can define coordinates in two formats:

    1. As an XML copied from the add-on web site (http://vaadin.com/addon/stepper):

      <dependency>
         <groupId>org.vaadin.addons</groupId>
         <artifactId>stepper</artifactId>
         <version>2.2.2</version>
      </dependency>
    2. In one line as you add dependencies in build.gradle: org.vaadin.addons:stepper:2.2.2

  • The Inherited widgetset field contains a widgetset name of the add-on:

    org.vaadin.risto.stepper.widgetset.StepperWidgetset
  • Integrate into generic UI - deselect this checkbox as we do not integrate the component into the Generic UI in this example.

Press the OK button.

If you open the project in the IDE, you can see that Studio has changed two files:

  1. In build.gradle, the web module now contains a dependency on the add-on that contains the component.

    configure(webModule) {
        ...
        dependencies {
            ...
            compile("org.vaadin.addons:stepper:2.2.2")
        }
  2. The AppWidgetSet.gwt.xml file of the web-toolkit module now inherits the add-on widgetset:

    <module>
        <inherits name="com.haulmont.cuba.web.toolkit.ui.WidgetSet" />
    
        <inherits name="org.vaadin.risto.stepper.widgetset.StepperWidgetset" />
    
        <set-property name="user.agent" value="safari" />
    Tip

    You can speed up the widgetset compilation by defining the user.agent property. In this example, widgetset will be compiled only for browsers based on WebKit: Chrome, Safari, etc.

Now the component from the Vaadin add-on is included to the project. Let’s see how to use it in the project screens.

  • Create a new entity Customer with two fields:

    • name of type String

    • score of type Integer

  • Generate standard screens for the new entity. Ensure that the In module field is set to Web Module. Screens that use Vaadin components directly must be placed in the web module.

    Tip

    Actually, screens can be placed in the gui module as well, but then the code that uses the Vaadin component should be moved to a separate companion.

  • Next, we will add the stepper component to the screen. You can place it in a FieldGroup or in a separate container. Let’s examine both methods.

    1. Add the custom = "true" attribute to the score field of the fieldGroup component of the customer-edit.xml screen.

      <?xml version="1.0" encoding="UTF-8" standalone="no"?>
      <window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
              caption="msg://editCaption"
              class="com.company.addondemo.web.customer.CustomerEdit"
              datasource="customerDs"
              focusComponent="fieldGroup"
              messagesPack="com.company.addondemo.web.customer">
          <dsContext>
              <datasource id="customerDs"
                          class="com.company.addondemo.entity.Customer"
                          view="_local"/>
          </dsContext>
          <layout expand="windowActions" spacing="true">
              <fieldGroup id="fieldGroup" datasource="customerDs">
                  <column width="250px">
                      <field property="name"/>
                      <field property="score" custom="true"/>
                  </column>
              </fieldGroup>
              <frame id="windowActions" screen="editWindowActions"/>
          </layout>
      </window>

      Add the following code to the CustomerEdit.java controller:

      package com.company.addondemo.web.customer;
      
      import com.haulmont.cuba.gui.components.AbstractEditor;
      import com.company.addondemo.entity.Customer;
      import com.haulmont.cuba.gui.components.Component;
      import com.haulmont.cuba.gui.components.FieldGroup;
      import com.haulmont.cuba.gui.components.VBoxLayout;
      import com.haulmont.cuba.gui.data.Datasource;
      import com.haulmont.cuba.gui.xml.layout.ComponentsFactory;
      import com.haulmont.cuba.web.gui.components.WebComponentsHelper;
      import com.vaadin.ui.Layout;
      import org.vaadin.risto.stepper.IntStepper;
      
      import javax.inject.Inject;
      import java.util.Map;
      
      public class CustomerEdit extends AbstractEditor<Customer> {
      
          @Inject
          private ComponentsFactory componentsFactory;
      
          @Inject
          private FieldGroup fieldGroup;
      
          @Inject
          private Datasource<Customer> customerDs;
      
          private IntStepper stepper = new IntStepper();
      
          @Override
          public void init(Map<String, Object> params) {
              fieldGroup.createField("score");
              Component box = componentsFactory.createComponent(VBoxLayout.class);
              fieldGroup.getFieldNN("score").setComponent(box);
              Layout layout = (Layout) WebComponentsHelper.unwrap(box);
              layout.addComponent(stepper);
              stepper.setSizeFull();
              stepper.addValueChangeListener(event ->
                      customerDs.getItem().setValue("score", event.getProperty().getValue())
              );
          }
      
          @Override
          protected void initNewItem(Customer item) {
              item.setScore(0);
          }
      
          @Override
          protected void postInit() {
              stepper.setValue(getItem().getScore());
          }
      }

      The init() method initializes the custom score field. The ComponentsFactory creates an instance of BoxLayout, retrieves a link to the Vaadin container via WebComponentsHelper, and adds the new component to it. The BoxLayout is then returned to be used in the custom field.

      Data binding is implemented programmatically by setting a current value to the stepper component from the edited Customer instance in the postInit() method. Additionally, the corresponding entity attribute is updated through the value change listener, when the user changes the value.

    2. The new component can be used in any part of the screen outside of the FieldGroup. In order to do this, declare the scoreBox container in the XML-descriptor:

      <?xml version="1.0" encoding="UTF-8" standalone="no"?>
      <window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
              caption="msg://editCaption"
              class="com.company.addondemo.web.customer.CustomerEdit"
              datasource="customerDs"
              focusComponent="fieldGroup"
              messagesPack="com.company.addondemo.web.customer">
          <dsContext>
              <datasource id="customerDs"
                          class="com.company.addondemo.entity.Customer"
                          view="_local"/>
          </dsContext>
          <layout expand="windowActions" spacing="true">
              <fieldGroup id="fieldGroup" datasource="customerDs">
                  <column width="250px">
                      <field property="name"/>
                  </column>
              </fieldGroup>
      
              <hbox id="scoreBox" spacing="true">
                  <label value="Score" align="MIDDLE_LEFT"/>
              </hbox>
      
              <frame id="windowActions" screen="editWindowActions"/>
          </layout>
      </window>

      Inject the container to the screen controller, retrieve a link to the underlying Vaadin container and add the component to it:

      package com.company.addondemo.web.customer;
      
      import com.haulmont.cuba.gui.components.*;
      import com.company.addondemo.entity.Customer;
      import com.haulmont.cuba.gui.data.Datasource;
      import com.haulmont.cuba.web.gui.components.WebComponentsHelper;
      import com.vaadin.ui.Layout;
      import org.vaadin.risto.stepper.IntStepper;
      
      import javax.inject.Inject;
      import java.util.Map;
      
      public class CustomerEdit extends AbstractEditor<Customer> {
      
          @Inject
          private FieldGroup fieldGroup;
      
          @Inject
          private Datasource<Customer> customerDs;
      
          @Inject
          private BoxLayout scoreBox;
      
          private IntStepper stepper = new IntStepper();
      
          @Override
          public void init(Map<String, Object> params) {
      
              Layout box = (Layout) WebComponentsHelper.unwrap(scoreBox);
              box.addComponent(stepper);
      
              fieldGroup.addField(fieldGroup.createField("score"));
      
              stepper.setSizeFull();
              stepper.addValueChangeListener(event ->
                      customerDs.getItem().setValue("score", event.getProperty().getValue())
              );
          }
      
          @Override
          protected void initNewItem(Customer item) {
              item.setScore(0);
          }
      
          @Override
          protected void postInit() {
              stepper.setValue(getItem().getScore());
          }
      }

      Data binding is implemented in the same way as described above.

  • To adapt the component style, create a theme extension in the project. Click the Create theme extension link in the Project properties navigator section. Select the halo theme. After that, open the themes/halo/halo-ext.scss file located in the web module and add the following code:

    @import "../halo/halo";
    
    /* Define your theme modifications inside next mixin */
    @mixin halo-ext {
        @include halo;
    
        /* Basic styles for stepper inner text box */
        .stepper input[type="text"] {
           @include box-defaults;
           @include valo-textfield-style;
           &:focus {
             @include valo-textfield-focus-style;
           }
        }
    }
  • Start the application server. The resulting editor screen will look as follows:

customer edit result
4.5.5.2. Integrating a Vaadin Component into the Generic UI

In the previous section, we have included the third-party Stepper component in the project. In this section, we will integrate it into CUBA Generic UI. This will allow developers to use the component declaratively in the screen XML and bind it to the data model entities through datasources.

Create a new project in CUBA Studio and name it addon-gui-demo. Type agd in the Project namespace field.

Create the web-toolkit module by clicking the Create web toolkit module link of the Project properties navigator section.

Then click the New UI component link. The UI component generation page will open. Select the Vaadin add-on value in the Component type section.

studio vaadin addon wizard gui

Fill in the Add-on Maven dependency and Inherited widgetset as described in the previous section.

Then fill in the fields of the bottom section:

  • Integrate into Generic UI - defines that a component should be integrated into the Generic UI.

  • Component XML element - an element to be used in screen XML descriptors. Enter stepper.

  • Component interface name - a name of the component Generic UI interface. Enter Stepper.

  • FQN of the Vaadin component from add-on - fully qualified class name of the Vaadin component from the add-on. In our case it is org.vaadin.risto.stepper.IntStepper.

When you click OK, Studio will do the following:

  • Add the Vaadin add-on as a web module dependency in build.gradle.

  • Include add-on widgetset in AppWidgetSet.gwt.xml of web-toolkit module.

  • Generate stubs for the following files:

    • Stepper - an interface of the component in the gui module.

    • WebStepper - a component implementation in the web module.

    • StepperLoader - a component XML-loader in the gui module.

    • ui-component.xsd - a new component XML schema definition. If the file already exists, the information about the new component will be added to the existing file.

    • cuba-ui-component.xml - the file that registers a new component loader in web module. If the file already exists, the information about the new component will be added to the existing file.

Open the project in the IDE.

Let’s walk through generated files add make necessary changes.

  • Open the Stepper interface in the gui module. Replace its content with the following code:

    package com.company.addonguidemo.gui.components;
    
    import com.haulmont.cuba.gui.components.Field;
    
    public interface Stepper extends Field {
    
        String NAME = "stepper";
    
        boolean isManualInputAllowed();
        void setManualInputAllowed(boolean value);
    
        boolean isMouseWheelEnabled();
        void setMouseWheelEnabled(boolean value);
    
        int getStepAmount();
        void setStepAmount(int amount);
    
        int getMaxValue();
        void setMaxValue(int maxValue);
    
        int getMinValue();
        void setMinValue(int minValue);
    }

    The base interface for the component is Field, which is designed to display and edit an entity attribute.

  • Open the WebStepper class - a component implementation in the web module. Replace its content with the following code:

    package com.company.addonguidemo.web.gui.components;
    
    import com.company.addonguidemo.gui.components.Stepper;
    import com.haulmont.cuba.web.gui.components.WebAbstractField;
    import org.vaadin.risto.stepper.IntStepper;
    
    public class WebStepper extends WebAbstractField<IntStepper> implements Stepper {
        public WebStepper() {
            this.component = new org.vaadin.risto.stepper.IntStepper();
        }
    
        @Override
        public boolean isManualInputAllowed() {
            return component.isManualInputAllowed();
        }
        @Override
        public void setManualInputAllowed(boolean value) {
            component.setManualInputAllowed(value);
        }
    
        @Override
        public boolean isMouseWheelEnabled() {
            return component.isMouseWheelEnabled();
        }
        @Override
        public void setMouseWheelEnabled(boolean value) {
            component.setMouseWheelEnabled(value);
        }
    
        @Override
        public int getStepAmount() {
            return component.getStepAmount();
        }
        @Override
        public void setStepAmount(int amount) {
            component.setStepAmount(amount);
        }
    
        @Override
        public int getMaxValue() {
            return component.getMaxValue();
        }
        @Override
        public void setMaxValue(int maxValue) {
            component.setMaxValue(maxValue);
        }
    
        @Override
        public int getMinValue() {
            return component.getMinValue();
        }
        @Override
        public void setMinValue(int minValue) {
            component.setMinValue(minValue);
        }
    }

    The chosen base class is WebAbstractField, which implements the methods of the Field interface.

  • The StepperLoader class in gui module loads the component from its representation in XML.

    package com.company.addonguidemo.gui.xml.layout.loaders;
    
    import com.company.addonguidemo.gui.components.Stepper;
    import com.haulmont.cuba.gui.xml.layout.loaders.AbstractFieldLoader;
    
    public class StepperLoader extends AbstractFieldLoader<Stepper> {
        @Override
        public void createComponent() {
            resultComponent = factory.createComponent(Stepper.class);
            loadId(resultComponent, element);
        }
    
        @Override
        public void loadComponent() {
    
            super.loadComponent();
    
            String manualInput = element.attributeValue("manualInput");
            if (manualInput != null) {
                resultComponent.setManualInputAllowed(Boolean.parseBoolean(manualInput));
            }
            String mouseWheel = element.attributeValue("mouseWheel");
            if (mouseWheel != null) {
                resultComponent.setMouseWheelEnabled(Boolean.parseBoolean(mouseWheel));
            }
            String stepAmount = element.attributeValue("stepAmount");
            if (stepAmount != null) {
                resultComponent.setStepAmount(Integer.parseInt(stepAmount));
            }
            String maxValue = element.attributeValue("maxValue");
            if (maxValue != null) {
                resultComponent.setMaxValue(Integer.parseInt(maxValue));
            }
            String minValue = element.attributeValue("minValue");
            if (minValue != null) {
                resultComponent.setMinValue(Integer.parseInt(minValue));
            }
        }
    }

    The AbstractFieldLoader class contains code for loading basic properties of the Field component. So StepperLoader loads only the specific properties of the Stepper component.

  • The cuba-ui-component.xml file in the web module registers the new component and its loader. Leave the file unchanged.

    <?xml version="1.0" encoding="UTF-8" standalone="no"?>
    <components>
        <component xmlns="http://schemas.haulmont.com/cuba/components.xsd">
            <name>stepper</name>
            <componentLoader>com.company.addonguidemo.gui.xml.layout.loaders.StepperLoader</componentLoader>
            <class>com.company.addonguidemo.web.gui.components.WebStepper</class>
        </component>
    </components>
  • The ui-component.xsd file in gui module contains XML schema definitions of custom visual components. Add the stepper attributes definition.

    <?xml version="1.0" encoding="UTF-8" standalone="no"?>
    <xs:schema xmlns="http://schemas.company.com/agd/0.1/ui-component.xsd"
               attributeFormDefault="unqualified"
               elementFormDefault="qualified"
               targetNamespace="http://schemas.company.com/agd/0.1/ui-component.xsd"
               xmlns:xs="http://www.w3.org/2001/XMLSchema">
        <xs:element name="stepper">
            <xs:complexType>
                <xs:attribute name="id" type="xs:string"/>
                <xs:attribute name="caption" type="xs:string"/>
                <xs:attribute name="width" type="xs:string"/>
                <xs:attribute name="height" type="xs:string"/>
                <xs:attribute name="datasource" type="xs:string"/>
                <xs:attribute name="property" type="xs:string"/>
                <xs:attribute name="manualInput" type="xs:boolean"/>
                <xs:attribute name="mouseWheel" type="xs:boolean"/>
                <xs:attribute name="stepAmount" type="xs:int"/>
                <xs:attribute name="maxValue" type="xs:int"/>
                <xs:attribute name="minValue" type="xs:int"/>
            </xs:complexType>
        </xs:element>
    </xs:schema>

Let’s see how to add the new component to a screen.

  • Create a new entity Customer. The entity have two fields:

    • name of type String

    • score of type Integer

  • Generate standard screens for the new entity.

  • Add the stepper component to the editor screen. You can place it in a FieldGroup or in a separate container. We’ll examine both methods.

    1. Using the component inside a container.

      • Open the customer-edit.xml file.

      • Define the new namespace xmlns:app="http://schemas.company.com/agd/0.1/ui-component.xsd".

      • Remove the score field from fieldGroup.

      • Add stepper component to the screen.

      As a result, the XML descriptor should look like this:

      <?xml version="1.0" encoding="UTF-8" standalone="no"?>
      <window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
              xmlns:app="http://schemas.company.com/agd/0.1/ui-component.xsd"
              caption="msg://editCaption"
              class="com.company.addonguidemo.gui.customer.CustomerEdit"
              datasource="customerDs"
              focusComponent="fieldGroup"
              messagesPack="com.company.addonguidemo.gui.customer">
          <dsContext>
              <datasource id="customerDs"
                          class="com.company.addonguidemo.entity.Customer"
                          view="_local"/>
          </dsContext>
          <layout expand="windowActions" spacing="true">
              <fieldGroup id="fieldGroup" datasource="customerDs">
                  <column width="250px">
                      <field property="name"/>
                  </column>
              </fieldGroup>
              <app:stepper id="stepper" datasource="customerDs" property="score" caption="Score"
                           minValue="1" maxValue="20"/>
              <frame id="windowActions" screen="editWindowActions"/>
          </layout>
      </window>

      In the example above, the stepper component is associated with the score attribute of the Customer entity. An instance of this entity is managed by the customerDs datasource.

    2. Using the new component inside a FieldGroup:

      <?xml version="1.0" encoding="UTF-8" standalone="no"?>
      <window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
              caption="msg://editCaption"
              class="com.company.addonguidemo.gui.customer.CustomerEdit"
              datasource="customerDs"
              focusComponent="fieldGroup"
              messagesPack="com.company.addonguidemo.gui.customer">
          <dsContext>
              <datasource id="customerDs"
                          class="com.company.addonguidemo.entity.Customer"
                          view="_local"/>
          </dsContext>
          <layout expand="windowActions" spacing="true">
              <fieldGroup id="fieldGroup" datasource="customerDs">
                  <column width="250px">
                      <field property="name"/>
                      <field property="score" custom="true"/>
                  </column>
              </fieldGroup>
              <frame id="windowActions" screen="editWindowActions"/>
          </layout>
      </window>
      package com.company.addonguidemo.web.customer;
      
      import com.company.addonguidemo.gui.components.Stepper;
      import com.haulmont.cuba.gui.components.AbstractEditor;
      import com.company.addonguidemo.entity.Customer;
      import com.haulmont.cuba.gui.components.FieldGroup;
      import com.haulmont.cuba.gui.data.Datasource;
      import com.haulmont.cuba.gui.xml.layout.ComponentsFactory;
      
      import javax.inject.Inject;
      import java.util.Map;
      
      public class CustomerEdit extends AbstractEditor<Customer> {
      
          @Inject
          private ComponentsFactory componentsFactory;
          @Inject
          private FieldGroup fieldGroup;
          @Inject
          private Datasource<Customer> customerDs;
      
          @Override
          public void init(Map<String, Object> params) {
              Stepper stepper = componentsFactory.createComponent(Stepper.class);
              stepper.setDatasource(customerDs, "score");
              stepper.setWidth("100%");
              fieldGroup.getFieldNN("score").setComponent(stepper);
          }
      }
  • To adapt the component style, create a theme extension in the project. Click the Create theme extension link in the Project properties navigator section. Select the halo theme. After that, open the themes/halo/halo-ext.scss file located in the web module and add the following code:

    @import "../halo/halo";
    
    /* Define your theme modifications inside next mixin */
    @mixin halo-ext {
        @include halo;
    
        /* Basic styles for stepper inner text box */
        .stepper input[type="text"] {
           @include box-defaults;
           @include valo-textfield-style;
           &:focus {
             @include valo-textfield-focus-style;
           }
        }
    }
  • Start the application server. The resulting editor screen will look as follows:

customer edit result
4.5.5.3. Using a JavaScript library

In this example, we will use the Slider component from the jQuery UI library. The slider will have two drag handlers that define a values range.

Create a new project in CUBA Studio and name it jscomponent.

Click the New UI component button on the Project properties navigator section. The UI component generation will open. Select the JavaScript component value in the Component type section.

studio js component wizard

Enter SliderServerComponent in the Vaadin component class name field.

Deselect the Integrate into Generic UI flag. The process of integration into the Generic UI is the same as described at Integrating a Vaadin Component into the Generic UI, so we won’t repeat it here.

After clicking the OK button Studio will generate the following files:

  • SliderServerComponent - a Vaadin component integrated with JavaScript.

  • SliderState - a state class of the Vaadin component.

  • slider-connector.js - a JavaScript connector for the Vaadin component.

Let’s examine the generated files and make necessary changes in the source code.

  • SlideState state class defines what data is transferred between the server and the client. In our case it is a minimal possible value, maximum possible value and selected values.

    package com.company.jscomponent.web.toolkit.ui.slider;
    
    import com.vaadin.shared.ui.JavaScriptComponentState;
    
    public class SliderState extends JavaScriptComponentState {
        public double[] values;
        public double minValue;
        public double maxValue;
    }
  • Vaadin server-side component SliderServerComponent.

    package com.company.jscomponent.web.toolkit.ui.slider;
    
    import com.vaadin.annotations.StyleSheet;
    import com.vaadin.ui.AbstractJavaScriptComponent;
    import com.vaadin.annotations.JavaScript;
    import elemental.json.JsonArray;
    
    @JavaScript({"slider-connector.js", "jquery-ui.js"})
    @StyleSheet({"jquery-ui.css"})
    public class SliderServerComponent extends AbstractJavaScriptComponent {
    
        public interface ValueChangeListener {
            void valueChanged(double[] newValue);
        }
    
        private ValueChangeListener listener;
    
        public SliderServerComponent() {
            addFunction("valueChanged", arguments -> {
                JsonArray array = arguments.getArray(0);
                double[] values = new double[2];
                values[0] = array.getNumber(0);
                values[1] = array.getNumber(1);
                listener.valueChanged(values);
            });
        }
    
        public void setValue(double[] value) {
            getState().values = value;
        }
    
        public double[] getValue() {
            return getState().values;
        }
    
        public double getMinValue() {
            return getState().minValue;
        }
    
        public void setMinValue(double minValue) {
            getState().minValue = minValue;
        }
    
        public double getMaxValue() {
            return getState().maxValue;
        }
    
        public void setMaxValue(double maxValue) {
            getState().maxValue = maxValue;
        }
    
        @Override
        protected SliderState getState() {
            return (SliderState) super.getState();
        }
    
        public ValueChangeListener getListener() {
            return listener;
        }
    
        public void setListener(ValueChangeListener listener) {
            this.listener = listener;
        }
    }

    The server component defines getters and setters to work with the slider state and an interface of value change listeners. The class extends AbstractJavaScriptComponent.

    The addFunction() method invocation in the class constructor defines a handler for an RPC-call of the valueChanged() function from the client.

    The @JavaScript and @StyleSheet annotations point to files that must be loaded on the web page. In our example, these are JavaScript files of the jquery-ui library, the connector and the stylesheet for jquery-ui. You should place these files to the Java package of the Vaadin server component.

Download an archive with jQuery UI from http://jqueryui.com/download and put files jquery-ui.js and jquery-ui.css from the archive to the Java package of the SliderServerComponent class. At the jQuery UI download page, you can select which components should be put into the archive. For this demo, it is enough to select only the Slider item of the Widgets group.

js project structure
  • JavaScript connector slider-connector.js.

    com_company_jscomponent_web_toolkit_ui_slider_SliderServerComponent = function() {
        var connector = this;
        var element = connector.getElement();
        $(element).html("<div/>");
        $(element).css("padding", "5px 10px");
    
        var slider = $("div", element).slider({
            range: true,
            slide: function(event, ui) {
                connector.valueChanged(ui.values);
            }
        });
    
        connector.onStateChange = function() {
            var state = connector.getState();
            slider.slider("values", state.values);
            slider.slider("option", "min", state.minValue);
            slider.slider("option", "max", state.maxValue);
            $(element).width(state.width);
        }
    }

    Connector is a function that initializes a JavaScript component when the web page is loaded. The function name must correspond to the server component class name where dots in package name are replaced with underscore characters.

    Vaadin adds several useful methods to the connector function. this.getElement() returns an HTML DOM element of the component, this.getState() returns a state object.

    Our connector does the following:

    • Initializes the slider component of the jQuery UI library. The slide() function is invoked when the position of any drag handler changes. This function in turn invokes the valueChanged() connector method. valuedChanged() is the method that we defined on the server side in the SliderServerComponent class.

    • Defines the onStateChange() function. It is called when the state object is changed on the server side.

To demonstrate how the component works, let’s create the Product entity with three attributes:

  • name of type String

  • minDiscount of type Double

  • maxDiscount of type Double

Generate standard screens for the entity. Ensure that the value of the In module field is Web Module.

The slider component will set minimal and maximum discount values of a product.

Open the product-edit.xml file. Make minDiscount and maxDiscount fields not editable by adding the editable="false" attribute to the corresponding elements. Then add the new custom slider field to the fieldGroup.

As a result, the XML descriptor of the editor screen should look as follows:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
        caption="msg://editCaption"
        class="com.company.jscomponent.web.product.ProductEdit"
        datasource="productDs"
        focusComponent="fieldGroup"
        messagesPack="com.company.jscomponent.web.product">
    <dsContext>
        <datasource id="productDs"
                    class="com.company.jscomponent.entity.Product"
                    view="_local"/>
    </dsContext>
    <layout expand="windowActions"
            spacing="true">
        <fieldGroup id="fieldGroup"
                    datasource="productDs">
            <column width="250px">
                <field property="name"/>
                <field property="minDiscount" editable="false"/>
                <field property="maxDiscount" editable="false"/>
                <field id="slider" custom="true"/>
            </column>
        </fieldGroup>
        <frame id="windowActions"
               screen="editWindowActions"/>
    </layout>
</window>

Open the ProductEit.java file. Replace its content with the following code:

package com.company.jscomponent.web.product;

import com.company.jscomponent.web.toolkit.ui.slider.SliderServerComponent;
import com.haulmont.cuba.gui.components.AbstractEditor;
import com.company.jscomponent.entity.Product;
import com.haulmont.cuba.gui.components.Component;
import com.haulmont.cuba.gui.components.FieldGroup;
import com.haulmont.cuba.gui.components.VBoxLayout;
import com.haulmont.cuba.gui.data.Datasource;
import com.haulmont.cuba.gui.xml.layout.ComponentsFactory;
import com.haulmont.cuba.web.gui.components.WebComponentsHelper;
import com.vaadin.ui.Layout;

import javax.inject.Inject;

public class ProductEdit extends AbstractEditor<Product> {

    @Inject
    private FieldGroup fieldGroup;

    @Inject
    private ComponentsFactory componentsFactory;

    @Inject
    private Datasource<Product> productDs;

    @Override
    protected void initNewItem(Product item) {
        super.initNewItem(item);
        item.setMinDiscount(15.0);
        item.setMaxDiscount(70.0);
    }

    @Override
    protected void postInit() {
        super.postInit();

        Component box = componentsFactory.createComponent(VBoxLayout.class);
        Layout vBox = (Layout) WebComponentsHelper.unwrap(box);
        SliderServerComponent slider = new SliderServerComponent();
        slider.setValue(new double[]{getItem().getMinDiscount(), getItem().getMaxDiscount()});
        slider.setMinValue(0);
        slider.setMaxValue(100);
        slider.setWidth("240px");
        slider.setListener(newValue -> {
            getItem().setMinDiscount(newValue[0]);
            getItem().setMaxDiscount(newValue[1]);
        });
        vBox.addComponent(slider);
        fieldGroup.getFieldNN("slider").setComponent(box);
    }
}

The initNewItem() method sets initial values for discounts of a new product.

Method init() initializes the slider custom field. It sets current, minimal and maximum values of the slider and defines the value change listener. When the drag handler moves, a new value will be set to the corresponding field of the editable entity.

Start the application server and open the product editor screen. Changing the drop handler position must change the value of the text fields.

product edit
4.5.5.4. Creating a GWT component

In this section, we will cover the creation of a simple GWT component (a rating field consisting of 5 stars) and its usage in application screens.

rating field component

Create a new project in CUBA Studio and name it ratingsample.

Click the Create web-toolkit module link in the Project properties navigator section.

Click the Create new UI component link. The New UI component page will open. Select the New GWT component value in the Component type section.

studio gwt component wizard

Enter RatingFieldServerComponent in the Vaadin component class name field.

Deselect the Integrate into Generic UI flag. The process of integration into the Generic UI is the same as described at Integrating a Vaadin Component into the Generic UI, so we won’t repeat it here.

After clicking the OK button Studio generates the following files:

  • RatingFieldWidget.java - a GWT widget in web-toolkit module.

  • RatingFieldServerComponent.java - a Vaadin component class.

  • RatingFieldState.java - a component state class.

  • RatingFieldConnector.java - a connector that links the client code with the server component.

  • RatingFieldServerRpc.java - a class that defines a server API for the client.

Let’s look at the generated files and make necessary changes in them.

  • RatingFieldWidget is a GWT widget. Replace its content with the following code:

    package com.company.ratingsample.web.toolkit.ui.client.ratingfield;
    
    import com.google.gwt.dom.client.DivElement;
    import com.google.gwt.dom.client.SpanElement;
    import com.google.gwt.dom.client.Style.Display;
    import com.google.gwt.user.client.DOM;
    import com.google.gwt.user.client.Event;
    import com.google.gwt.user.client.ui.FocusWidget;
    
    import java.util.ArrayList;
    import java.util.List;
    
    public class RatingFieldWidget extends FocusWidget {
    
        private static final String CLASSNAME = "ratingfield";
    
        // API for handle clicks
        public interface StarClickListener {
            void starClicked(int value);
        }
    
        protected List<SpanElement> stars = new ArrayList<SpanElement>(5);
        protected StarClickListener listener;
        protected int value = 0;
    
        public RatingFieldWidget() {
            DivElement container = DOM.createDiv().cast();
            container.getStyle().setDisplay(Display.INLINE_BLOCK);
            for (int i = 0; i < 5; i++) {
                SpanElement star = DOM.createSpan().cast();
    
                // add star element to the container
                DOM.insertChild(container, star, i);
                // subscribe on ONCLICK event
                DOM.sinkEvents(star, Event.ONCLICK);
    
                stars.add(star);
            }
            setElement(container);
    
            setStylePrimaryName(CLASSNAME);
        }
    
        // main method for handling events in GWT widgets
        @Override
        public void onBrowserEvent(Event event) {
            super.onBrowserEvent(event);
    
            switch (event.getTypeInt()) {
                // react on ONCLICK event
                case Event.ONCLICK:
                    SpanElement element = event.getEventTarget().cast();
                    // if click was on the star
                    int index = stars.indexOf(element);
                    if (index >= 0) {
                        int value = index + 1;
                        // set internal value
                        setValue(value);
    
                        // notify listeners
                        if (listener != null) {
                            listener.starClicked(value);
                        }
                    }
                    break;
            }
        }
    
        @Override
        public void setStylePrimaryName(String style) {
            super.setStylePrimaryName(style);
    
            for (SpanElement star : stars) {
                star.setClassName(style + "-star");
            }
    
            updateStarsStyle(this.value);
        }
    
        // let application code change the state
        public void setValue(int value) {
            this.value = value;
            updateStarsStyle(value);
        }
    
        // refresh visual representation
        private void updateStarsStyle(int value) {
            for (SpanElement star : stars) {
                star.removeClassName(getStylePrimaryName() + "-star-selected");
            }
    
            for (int i = 0; i < value; i++) {
                stars.get(i).addClassName(getStylePrimaryName() + "-star-selected");
            }
        }
    }

    A widget is a client-side class responsible for displaying the component in the web browser and handling events. It defines interfaces for working with the server side. In our case these are the setValue() method and the StarClickListener interface.

  • RatingFieldServerComponent is a Vaadin component class. It defines an API for the server code, accessor methods, event listeners and data sources connection. Developers use the methods of this class in the application code.

    package com.company.ratingsample.web.toolkit.ui;
    
    import com.company.ratingsample.web.toolkit.ui.client.ratingfield.RatingFieldServerRpc;
    import com.company.ratingsample.web.toolkit.ui.client.ratingfield.RatingFieldState;
    import com.vaadin.ui.AbstractField;
    
    // the field will have a value with integer type
    public class RatingFieldServerComponent extends AbstractField<Integer> {
    
        public RatingFieldServerComponent() {
            // register an interface implementation that will be invoked on a request from the client
            registerRpc((RatingFieldServerRpc) value -> setValue(value, true));
        }
    
        // field value type
        @Override
        public Class<? extends Integer> getType() {
            return Integer.class;
        }
    
        // define own state class
        @Override
        protected RatingFieldState getState() {
            return (RatingFieldState) super.getState();
        }
    
        @Override
        protected RatingFieldState getState(boolean markAsDirty) {
            return (RatingFieldState) super.getState(markAsDirty);
        }
    
        // we need to refresh the state when setValue is invoked from the application code
        @Override
        protected void setInternalValue(Integer newValue) {
            super.setInternalValue(newValue);
            if (newValue == null) {
                newValue = 0;
            }
            getState().value = newValue;
        }
    }
  • The RatingFieldState state class defines what data are sent between the client and the server. It contains public fields that are automatically serialized on server side and deserialized on the client.

    package com.company.ratingsample.web.toolkit.ui.client.ratingfield;
    
    import com.vaadin.shared.AbstractFieldState;
    
    public class RatingFieldState extends AbstractFieldState {
        {   // change the main style name of the component
            primaryStyleName = "ratingfield";
        }
        // define a field for the value
        public int value = 0;
    }
  • The RatingFieldServerRpc interface defines a server API that is used from the client-side. Its methods may be invoked by the RPC mechanism built into Vaadin. We will implement this interface in the component.

    package com.company.ratingsample.web.toolkit.ui.client.ratingfield;
    
    import com.vaadin.shared.communication.ServerRpc;
    
    public interface RatingFieldServerRpc extends ServerRpc {
        //method will be invoked in the client code
        void starClicked(int value);
    }
  • The RatingFieldConnector connector links client code with the server.

    package com.company.ratingsample.web.toolkit.ui.client.ratingfield;
    
    import com.company.ratingsample.web.toolkit.ui.RatingFieldServerComponent;
    import com.company.ratingsample.web.toolkit.ui.client.ratingfield.RatingFieldServerRpc;
    import com.company.ratingsample.web.toolkit.ui.client.ratingfield.RatingFieldState;
    import com.vaadin.client.communication.StateChangeEvent;
    import com.vaadin.client.ui.AbstractFieldConnector;
    import com.vaadin.shared.ui.Connect;
    
    // link the connector with the server implementation of RatingField
    // extend AbstractField connector
    @Connect(RatingFieldServerComponent.class)
    public class RatingFieldConnector extends AbstractFieldConnector {
    
        // we will use a RatingFieldWidget widget
        @Override
        public RatingFieldWidget getWidget() {
            RatingFieldWidget widget = (RatingFieldWidget) super.getWidget();
    
            if (widget.listener == null) {
                widget.listener = new RatingFieldWidget.StarClickListener() {
                    @Override
                    public void starClicked(int value) {
                        getRpcProxy(RatingFieldServerRpc.class).starClicked(value);
                    }
                };
            }
            return widget;
        }
    
        // our state class is RatingFieldState
        @Override
        public RatingFieldState getState() {
            return (RatingFieldState) super.getState();
        }
    
        // react on server state change
        @Override
        public void onStateChanged(StateChangeEvent stateChangeEvent) {
            super.onStateChanged(stateChangeEvent);
    
            // refresh the widget if the value on server has changed
            if (stateChangeEvent.hasPropertyChanged("value")) {
                getWidget().setValue(getState().value);
            }
        }
    }

The RatingFieldWidget class does not define the component appearance, it only assigns style names to key elements. To define an appearance of the component, we’ll create stylesheet files. Click the Create theme extension link on the Project properties navigator section. Select the halo theme in the dialog. Studio creates SCSS files for theme extension in the themes directory of the web module. The halo theme uses FonAwesome font glyphs instead of icons. We’ll use this fact.

It is recommended to put component styles into a separate file componentname.scss in the components/componentname directory in the form of SCSS mixture. Create the components/ratingfield directories structure in the themes/halo directory of the web module. Then create the ratingfield.scss file inside the ratingfield directory:

gwt theme ext structure
@mixin ratingfield($primary-stylename: ratingfield) {
  .#{$primary-stylename}-star {
    font-family: FontAwesome;
    font-size: $v-font-size--h2;
    padding-right: round($v-unit-size/4);
    cursor: pointer;

    &:after {
          content: '\f006'; // 'fa-star-o'
    }
  }

  .#{$primary-stylename}-star-selected {
    &:after {
          content: '\f005'; // 'fa-star'
    }
  }

  .#{$primary-stylename} .#{$primary-stylename}-star:last-child {
    padding-right: 0;
  }

  .#{$primary-stylename}.v-disabled .#{$primary-stylename}-star {
    cursor: default;
  }
}

Include this file in the `halo-ext.scss`main theme file:

@import "../halo/halo";

@import "components/ratingfield/ratingfield";

/* Define your theme modifications inside next mixin */
@mixin halo-ext {
  @include halo;

  @include ratingfield;
}

To demonstrate how the component works let’s create a new screen in the web module.

Name the screen file rating-screen.xml.

gwt rating screen designer

Add the screen to the application menu. Go to the Main menu navigator section and click the Edit button. The menu editor will open. Add the screen created earlier to the application menu.

Open the rating-screen.xml file in the IDE. We need a container for our component. Declare it in the screen XML:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
        caption="msg://caption"
        class="com.company.ratingsample.web.screens.RatingScreen"
        messagesPack="com.company.ratingsample.web.screens">
    <layout expand="container">
        <vbox id="container">
            <!-- we'll add vaadin component here-->
        </vbox>
    </layout>
</window>

Open the RatingScreen.java screen controller and add the code that puts the component to the screen.

package com.company.ratingsample.web.screens;

import com.company.ratingsample.web.toolkit.ui.RatingFieldServerComponent;
import com.haulmont.cuba.gui.components.AbstractWindow;
import com.haulmont.cuba.gui.components.BoxLayout;
import com.haulmont.cuba.web.gui.components.WebComponentsHelper;
import com.vaadin.ui.Layout;

import javax.inject.Inject;
import java.util.Map;

public class RatingScreen extends AbstractWindow {
    @Inject
    private BoxLayout container;

    @Override
    public void init(Map<String, Object> params) {
        super.init(params);
        com.vaadin.ui.Layout containerLayout = (Layout) WebComponentsHelper.unwrap(container);
        RatingFieldServerComponent field = new RatingFieldServerComponent();
        field.setCaption("Rate this!");
        containerLayout.addComponent(field);
    }
}

Start the application server and see the result.

rating screen result
4.5.5.5. Support for Custom Visual Components in CUBA Studio

This section describes how to integrate a new custom visual component into CUBA Studio. As a result of the integration, the component will be available on the component palette of the WYSIWYG screen layout designer. Developers will be able to drag and drop the component to the canvas and edit its properties in the properties panel.

Let’s walk through the process of integrating the stepper component into Studio. Creation of this component was described in Integrating a Vaadin Component into the Generic UI.

Open the project containing the stepper component.

Tip

If you didn’t create this project, you can still reproduce the steps listed below in a new project. In this case, you will see how Studio supports the component, but you won’t be able to run the application.

Click the Extend Studio link on the Project properties navigator section.

ui component extension window

In the Extend Studio screen, fill in the following fields:

  • Configuration name - a configuration identifier. Enter stepper.

  • Component XML element - a component element name to use in screen XML descriptors. It is stepper in our case.

    The Component class name and Component model class name fields are filled automatically based on the value of the Component XML element. Leave the values as is.

  • Component namespace URI - a namespace from the XSD that describes the Generic UI component. If you’ve generated the new component with Studio, then you can take the value of this field from the ui-component.xsd file located in the gui module.

  • Component namespace prefix - a prefix for the component XML element.

  • Standard properties - standard properties that should be available for editing in the component properties panel of the Studio screen layout designer.

    Select caption, datasource and property checkboxes.

    Tip

    id, align, height, width, enable, stylename and visible properties are available to any component by default.

  • Custom properties - this table is used for declaring component specific properties that should be edited in the component properties panel.

    Add the following properties:

    • manualInput of type Boolean, the default value is true

    • mouseWheel of type Boolean, the default value is true

    • stepAmount, of type Integer, the default value is 0

    • maxValue, of type Integer, the default value is 0

    • minValue, of type Integer, the default value is 0

Press the OK button.

The custom visual components support is initialized when the Studio server start. Go to the Studio server window, stop the server, exit Studio, then reopen and start it again.

Re-create standard screens for the Customer entity to erase the results of our previous experiments.

Then go to the GENERIC UI navigator section and open the customer-edit.xml screen.

Remove the score field from fieldGroup because we will use a separate component for editing the score.

Find the new Stepper component on the components palette, then drag it to the screen below fieldGroup.

stepper in palette

Select the stepper component and go to the component Properties tab.

stepper component properties

Fill in the fields:

  • id - stepper

  • caption - Stepper

  • datasource - customerDs

  • property - score

  • maxValue - 50

Go to the XML tab to see the result.

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
        caption="msg://editCaption"
        class="com.company.addonguidemo.gui.customer.CustomerEdit"
        datasource="customerDs"
        focusComponent="fieldGroup"
        messagesPack="com.company.addonguidemo.gui.customer"
        xmlns:app="http://schemas.company.com/agd/0.1/ui-component.xsd">
    <dsContext>
        <datasource id="customerDs"
                    class="com.company.addonguidemo.entity.Customer"
                    view="_local"/>
    </dsContext>
    <layout expand="windowActions"
            spacing="true">
        <fieldGroup id="fieldGroup"
                    datasource="customerDs">
            <column width="250px">
                <field property="name"/>
            </column>
        </fieldGroup>
        <app:stepper id="stepper"
                     caption="Stepper"
                     datasource="customerDs"
                     maxValue="50"
                     property="score"/>
        <frame id="windowActions"
               screen="editWindowActions"/>
    </layout>
</window>

There is a new namespace with the app prefix in the screen XML, the stepper component is added to the screen, and its properties are set correctly.

5. The Framework

This chapter contains detailed description of the platform architecture, components and mechanisms.

5.1. Architecture

This section covers the architecture of CUBA applications in different perspectives: by tiers, blocks, modules and components.

5.1.1. Application Tiers and Blocks

The platform enables building applications according to the classic three-tier pattern: client tier, middleware, database. The tier indicates the degree of "remoteness" from the stored data.

Further on, mainly middleware and client tiers will be described, therefore the words "all tiers" will refer to these tiers only.

Each tier enables creating one or more application blocks. A block is a separate executable program interacting with other blocks in the application. CUBA platform tools enable creation of blocks in the form of web or desktop applications.

AppTiers
Figure 6. Application Tiers and Blocks
Middleware

The middle tier contains core business logic of the application and provides access to the database. It is represented by a separate web application running on Java EE Web Profile standard container. See Middleware Components.

Web Client

A main block of the client tier. It contains the interface designed primarily for internal users. It is represented by a separate web application running on Java EE Web Profile standard container. The user interface is implemented on the base of Vaadin framework. See Generic User Interface.

Desktop Client

An additional block of the client tier. It contains the interface designed primarily for internal users. It is represented by a desktop Java application; the user interface is implemented on the base of Java Swing framework. See Generic User Interface.

Web Portal

An additional block of the client tier. It can contain an interface for external users and entry points for integration with mobile devices and third-party applications. It is represented by a separate web application running on Java EE Web Profile standard container. The user interface is implemented on the base of Spring MVC framework. See Portal Components.

Polymer Client

An optional client designed for external users and written in pure JavaScript. It is based on Google Polymer framework and communicates with the middleware via REST API running either in Web Client or in Web Portal blocks. See Polymer User Interface.

Middleware is the mandatory block for any application. User interface can be implemented by one or several blocks, such as Web Client and Polymer Client.

All of the Java-based client blocks interact with the middle tier uniformly via HTTP protocol enabling to deploy the middle tier arbitrarily, behind firewall as well. In the simplest case when the middle tier and the web client are deployed on the same server local interaction between them can bypass the network stack for better performance.

5.1.2. Application Modules

A module is the smallest structural part of CUBA application. It is a single module of application project and the corresponding JAR file with executable code.

Standard modules:

  • global – includes entity classes, service interfaces, and other classes common for all tiers. It is used in all application blocks.

  • core – implements services and all other components of the middle tier. It is used only in Middleware.

  • gui – common components of the generic user interface. It is used in Web Client and Desktop Client.

  • web – the implementation of the generic user interface based on Vaadin and other specific web client classes. It is used in Web Client block.

  • desktop – an optional module – implementation of generic user interface based on Java Swing, as well as other specific desktop client classes. It is used in Desktop Client block.

  • portal – an optional module – implementation of Web portal based on Spring MVC.

  • polymer-client – an optional module – implementation of Polymer User Interface in JavaScript.

AppModules
Figure 7. Application Modules

5.1.3. Application Components

The functionality of the platform is divided into several application components (aka base projects):

  • cuba – the main component containing all of the functionality described in this manual.

  • reports – reports generating subsystem.

  • fts – full-text search subsystem.

  • charts – subsystem for displaying charts and maps.

  • bpm – the mechanism of business processes execution according to the BPMN 2.0 standard.

An application always project depends on one ore more application components. It means that the application uses the component as a library and includes its functionality.

Any CUBA application depends on the cuba component. Other platform components are optional and can be included to the application only if needed. All optional components depend on cuba and can also contain dependencies between each other.

Below is a diagram showing dependencies between the platform application components. Solid lines demonstrate mandatory dependencies, dashed lines mean optional ones.

BaseProjects

Any CUBA application can, in turn, be used as a component of another application. It enables decomposition of large projects to a set of functional modules, which can be developed independently. You can also create a set of custom application components in your organization and use them in multiple projects, effectively creating your own higher-level platform on top of CUBA. Below is an example of a possible structure of dependencies between application components.

AppComponents2

In order to be used as a component, an application project should contain an app-component.xml descriptor and a special entry in the manifest of the global module JAR. CUBA Studio allows you to generate the descriptor and manifest entry for the current project automatically.

See step-by-step guide to working with a custom application component in the Using Application Components section.

5.1.4. Application Structure

The above-listed architectural principles are directly reflected in the composition of assembled application. Let us consider the example of a simple application sales, which has two blocks – Middleware and Web Client; and includes functionality of the two application components - cuba and reports.

SampleAppArtifacts
Figure 8. The structure of a simple application

The figure demonstrates the contents of several directories of the Tomcat server with a deployed application sales in it.

The Middleware block is represented by the app-core web application, the Web Client block – by the app web application. The executable code of the web applications can be found in directories WEB-INF/lib in sets of JAR files. Each JAR (artifact) is a result of assembly of one of the application or a component modules.

For instance, the contents of JAR files of the web application in middle tier app-core is determined by the facts that the Middleware block includes global and core modules, and the application uses cuba and reports components.

5.2. Common Components

This chapter covers platform components, which are common for all tiers of the application.

5.2.1. Data Model

Data model entities are divided into two categories:

  • Persistent – instances of such entities are stored in the database using ORM.

  • Non-persistent – instances exist only in memory, or are stored somewhere via different mechanisms.

The entities are characterized by their attributes. An attribute corresponds to a field and a pair of access methods (get / set) of the field. If the setter is omitted, the attribute becomes read only.

Persistent entities may include attributes that are not stored in the database. For a non-persistent attribute, the field is optional and you can create only access methods.

The entity class should meet the following requirements:

  • Be inherited from one of the base classes provided by the platform (see below).

  • Have a set of fields and access methods corresponding to attributes.

  • The class and its fields (or access methods if the attribute has no corresponding field) must be annotated to provide information for the ORM (in case of a persistent entity) and metadata frameworks.

The following types can be used for entity attributes:

  • java.lang.String

  • java.lang.Boolean

  • java.lang.Integer

  • java.lang.Long

  • java.lang.Double

  • java.math.BigDecimal

  • java.util.Date

  • java.sql.Date

  • java.sql.Time

  • java.util.UUID

  • byte[]

  • enum

  • Entity

Base entity classes (see below) override equals() and hashCode() methods to check entity instances equivalence by comparing their identifiers. I.e., instances of the same class are considered equal, if their identifiers are equal.

5.2.1.1. Base Entity Classes

The base entity classes and interfaces are described in detail in this section.

EntityClasses
  • Instance – declares the basic methods for working with objects of application domain:

    • getting references to the object meta-class;

    • generating the instance name;

    • reading/writing attribute values by name;

    • adding listeners receiving notifications about attribute changes.

  • Entity – extends Instance with entity identifier; at the same time Entity does not define the type of the identifier leaving this option to descendants.

  • AbstractInstance – implements the logic of working with attribute change listeners.

    Warning

    AbstractInstance stores the listeners in WeakReference, and if there are no external references to the added listener, it will be immediately destroyed by garbage collector. Normally, attribute change listeners are visual components and UI datasources that are always referenced by other objects, so there is no problem with listeners dropout. However, if a listener is created by application code and no objects refer to it in a natural way, it is necessary to save it in a certain object field apart from just adding it to Instance.

  • AbstractNotPersistentEntity – base class of non-persistent entities.

  • BaseGenericIdEntity – base class of persistent entities. It implements Entity but does not specify the type of the identifier (i.e. the primary key) of the entity.

  • EmbeddableEntity - base class of embeddable persistent entities.

Below we consider base classes recommended for inheriting your entities from.

StandardEntity

Inherit from StandardEntity if you want a standard set of features: the primary key of UUID type, the instances have information on who and when created and modified them, require optimistic locking and soft deletion.

EntityClasses Standard
  • HasUuid – interface for entities having a globally unique identifier.

  • Versioned – interface for entities supporting optimistic locking.

  • Creatable – interface for entities that keep the information about when and by whom the instance was created.

  • Updatable – interface for entities that keep the information about when and by whom the instance was last changed.

  • SoftDelete – interface for entities supporting soft deletion.

BaseUuidEntity

Inherit from BaseUuidEntity if you want an entity with the primary key of UUID type but you don’t need all features of StandardEntity. You can implement some of the interfaces Creatable, Versioned, etc. in your concrete entity class.

EntityClasses Uuid
BaseLongIdEntity

Inherit from BaseLongIdEntity or BaseIntegerIdEntity if you want an entity with the primary key of the Long or Integer type. You can implement some of the interfaces Creatable, Versioned, etc. in your concrete entity class. Implementing HasUuid is highly recommended, as it enables some optimizations and allows you to identify your instances uniquely in a distributed environment.

EntityClasses Long
BaseStringIdEntity

Inherit from BaseStringIdEntity if you want an entity with the primary key of the String type. You can implement some of the interfaces Creatable, Versioned, etc. in your concrete entity class. Implementing HasUuid is highly recommended, as it enables some optimizations and allows you to identify your instances uniquely in a distributed environment. The concrete entity class must have a string field annotated with the @Id JPA annotation.

EntityClasses String
BaseIdentityIdEntity

Inherit from BaseIdentityIdEntity if you need to map the entity to a table with IDENTITY primary key. You can implement some of the interfaces Creatable, Versioned, etc. in your concrete entity class. Implementing HasUuid is highly recommended, as it enables some optimizations and allows you to identify your instances uniquely in a distributed environment. The id attribute of the entity (i.e. getId/setId methods) will be of type IdProxy which is designed to substitute the real identifier until it is generated by the database on insert.

EntityClasses Identity
BaseGenericIdEntity

Inherit from BaseGenericIdEntity directly if you need to map the entity to a table with a composite key. In this case, the concrete entity class must have a field of the embeddable type representing the key, annotated with the @EmbeddedId JPA annotation.

5.2.1.2. Entity Annotations

This section describes all annotations of entity classes and attributes supported by the platform.

Annotations from the javax.persistence package are required for JPA, annotations from com.haulmont.* packages are designed for metadata management and other mechanisms of the platform.

In this manual, if an annotation is identified by a simple class name, it refers to a platform class, located in one of the com.haulmont.* packages.

5.2.1.2.1. Class Annotations
@Embeddable

Defines an embedded entity stored in the same table as the owning entity.

@MetaClass annotation should be used to specify the entity name.

@EnableRestore

Indicates that the entity instances are available for recovery after soft deletion on the core$Entity.restore screen available through the Administration > Data Recovery main menu item.

@Entity

Declares a class to be a data model entity.

Parameters:

  • name – the name of the entity, must begin with a prefix, separated by a $ sign. It is recommended to use a short name of the project as a prefix to form a separate namespace.

Example:

@Entity(name = "sales$Customer")
@Extends

Indicates that the entity is an extension and it should be used everywhere instead of the base entity. See Functionality Extension.

@DiscriminatorColumn

Is used for defining a database column responsible for the distinction of entity types in the cases of SINGLE_TABLE and JOINED inheritance strategies.

Parameters:

  • name – the discriminator column name

  • discriminatorType – the discriminator column type

Example:

@DiscriminatorColumn(name = "TYPE", discriminatorType = DiscriminatorType.INTEGER)
@DiscriminatorValue

Defines the discriminator column value for this entity.

Example:

@DiscriminatorValue("0")
@Inheritance

Defines the inheritance strategy to be used for an entity class hierarchy. It is specified on the entity class that is the root of the entity class hierarchy.

Parameters:

  • strategy – inheritance strategy, SINGLE_TABLE by default

@Listeners

Defines the list of listeners intended for reaction to the entity instance lifecycle events on the middle tier.

The annotation value should be a string or an array of strings containing bean names of the listeners. See Entity Listeners.

Examples:

@Listeners("sample_UserEntityListener")
@Listeners({"sample_FooListener","sample_BarListener"})
@MappedSuperclass

Defines that the class is an ancestor for some entities and its attributes must be used as part of descendant entities. Such class is not associated with any particular database table.

@MetaClass

Is used for declaring non-persistent or embedded entity (meaning that @javax.persistence.Entity annotation cannot be applied)

Parameters:

  • name – the entity name, must begin with a prefix, separated by a $ sign. It is recommended to use a short name of the project as prefix to form a separate namespace.

Example:

@MetaClass(name = "sales$Customer")
@NamePattern

Determines the way of getting the name of the instance returned by the Instance.getInstanceName() method.

The annotation value should be a string in the {0}|{1} format, where:

  • {0} – formatting string according to the String.format() rules, or this object method name with the prefix #. The method should return String and should have no parameters.

  • {1} – a list of field names separated by commas, corresponding to {0} format. If the method is used in {0}, the list of fields is still required as it forms the _minimal view.

Examples:

@NamePattern("%s|name")
@NamePattern("#getCaption|login,name")
@PostConstruct

This annotation can be specified for a method. Such method will be invoked right after the entity instance is created by the Metadata.create() method. This is convenient when instance initialization requires invocation of managed beans. For example, see Entity Fields Initialization.

@PrimaryKeyJoinColumn

Is used in the case of JOINED inheritance strategy to specify a foreign key column for the entity which refers to the primary key of the ancestor entity.

Parameters:

  • name – the name of the foreign key column of the entity

  • referencedColumnName – the name of primary key column of the ancestor entity

Example:

@PrimaryKeyJoinColumn(name = "CARD_ID", referencedColumnName = "ID")
@SystemLevel

Indicates that the entity is system only and should not be available for selection in various lists of entities, such as generic filter parameter types or dynamic attribute type.

@Table

Defines database table for the given entity.

Parameters:

  • name – the table name

Example:

@Table(name = "SALES_CUSTOMER")
@TrackEditScreenHistory

Indicates that editor screens opening history will be recorded with the ability to display it on the sec$ScreenHistory.browse screen available through the Help > History main menu item.

5.2.1.2.2. Attribute Annotations

Attribute annotations should be set for the corresponding fields, with the following exception: if there is a need to declare read-only, non-persistent attribute foo, it is sufficient to create getFoo() method and annotate it with @MetaProperty.

@CaseConversion

Indicates that automatic case conversion should be used for text input fields bound with annotated entity attribute.

Parameters:

  • type - the conversion type: UPPER (default), LOWER.

Example:

@CaseConversion(type = ConversionType.UPPER)
@Column(name = "COUNTRY_CODE")
protected String countryCode;
@Column

Defines DB column for storing attribute values.

Parameters:

  • name – the column name.

  • length – (optional parameter, 255 by default) – the length of the column. It is also used for metadata generation and ultimately, can limit the maximum length of the input text in visual components bound to this attribute. Add the @Lob annotation to remove restriction on the attribute length.

  • nullable – (optional parameter, true by default) – determines if an attribute can contain null value. When nullable = false JPA ensures that the field has a value when saved. In addition, visual components working with the attribute can request the user to enter a value.

@Composition

Indicates that the relationship is a composition, which is a stronger variant of the association. Essentially this means that the related entity should only exist as a part of the owning entity, i.e. be created and deleted together with it.

For example, a list of items in an order (Order class contains a collection of Item instances):

@OneToMany(mappedBy = "order")
@Composition
protected List<Item> items;

Another example is a one-to-one relationship:

@Composition
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "DETAILS_ID")
protected CustomerDetails details;

Choosing @Composition annotation as the relationship type enables making use of a special commit mode for datasources in edit screens. In this mode, the changes to related instances are only stored when the master entity is committed. See Composite Structures for details.

@Embedded

Defines a reference attribute of embeddable type. The referenced entity should have @Embeddable annotation.

Example:

@Embedded
protected Address address;
@EmbeddedParameters

By default, ORM does not create an instance of embedded entity if all its attributes are null in the database. You can use the @EmbeddedParameters annotation to specify a different behavior when an instance is always non-null, for example:

@Embedded
@EmbeddedParameters(nullAllowed = false)
protected Address address;
@Id

Indicates that the attribute is the entity primary key. Typically, this annotation is set on the field of a base class, such as BaseUuidEntity. Using this annotation for a specific entity class is required only in case of inheritance from the BaseStringIdEntity base class (i.e. creating an entity with a string primary key).

@IgnoreUserTimeZone

Makes the platform to ignore the user’s time zone (if it is set for the current session) for an attribute of the timestamp type (annotated with @javax.persistence.Temporal.TIMESTAMP).

@JoinColumn

Defines DB column that determines the relationship between entities. Presence of this annotation indicates the owning side of the association.

Parameters:

  • name – the column name

Example:

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "CUSTOMER_ID")
protected Customer customer;
@JoinTable

Defines a join table on the owning side of @ManyToMany relationship.

Parameters:

  • name – the join table name

  • joinColumns@JoinColumn element in the join table corresponding to primary key of the owning side of the relationship (the one containing @JoinTable annotation)

  • inverseJoinColumns@JoinColumn element in the join table corresponding to primary key of the non-owning side of the relationship.

Example of the customers attribute of the Group class on the owning side of the relationship:

@ManyToMany
@JoinTable(name = "SALES_CUSTOMER_GROUP_LINK",
 joinColumns = @JoinColumn(name = "GROUP_ID"),
 inverseJoinColumns = @JoinColumn(name = "CUSTOMER_ID"))
protected Set<Customer> customers;

Example of the groups attribute of the Customer class on non-owning side of the same relationship:

@ManyToMany(mappedBy = "customers")
protected Set<Group> groups;
@Lob

Indicates that the attribute does not have any length restrictions. This annotation is used together with the @Column annotation. If @Lob is set, the default or explicitly defined length in @Column is ignored.

Example:

@Column(name = "DESCRIPTION")
@Lob
private String description;
@LocalizedValue

Determines a method for retrieving a localized value for an attribute, using MessageTools`.getLocValue()` method.

Parameters:

  • messagePack – explicit indication of the package, from which a localized message will be taken, for example, com.haulmont.cuba.core.entity.

  • messagePackExpr – expression defining the path to the attribute, containing a package name from which the localized message should be taken (for example, proc.messagesPack). The path starts from the attribute of the current entity.

The annotation in the example below indicates that localized message for the state attribute value should be taken from the package name defined in the messagesPack attribute of the proc entity.

@Column(name = "STATE")
@LocalizedValue(messagePackExpr = "proc.messagesPack")
protected String state;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "PROC_ID")
protected Proc proc;
@Lookup

Defines the lookup type settings for the reference attributes.

Parameters:

  • type - the default value is SCREEN, so a reference is selected from a lookup screen. The DROPDOWN value enables to select the reference from a drop-down list. If the lookup type is set to DROPDOWN, Studio will generate options datasource when scaffolding editor screen. Thus, the Lookup type parameter should be set before generation of an entity editor screen. Besides, the Filter component will allow a user to select parameter of this type from a drop-down list instead of lookup screen.

  • actions - defines the actions to be used in a PickerField component inside the FieldGroup by default. Possible values: lookup, clear, open.

@Lookup(type = LookupType.DROPDOWN, actions = {"open"})
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "CUSTOMER_ID")
protected Customer customer;
@ManyToMany

Defines a collection attribute with many-to-many relationship type.

Many-to-many relationship always has an owning side and can also have inverse, non-owning side. The owning side should be marked with additional @JoinTable annotation, and the non-owning side – with mappedBy parameter.

Parameters:

  • mappedBy – the field of the referenced entity, which owns the relationship. It must only be set on the non-owning side of the relationship.

  • targetEntity – the type of referenced entity. This parameter is optional if the collection is declared using Java generics.

  • fetch – (optional parameter, LAZY by default) – determines whether JPA will eagerly fetch the collection of referenced entities. This parameter should always remain LAZY, since retrieval of referenced entities in CUBA-application is determined dynamically by the views mechanism.

@ManyToOne

Defines a reference attribute with many-to-one relationship type.

Parameters:

  • fetch – (EAGER by default) parameter that determines whether JPA will eagerly fetch the referenced entity. This parameter should always be set to LAZY, since retrieval of referenced entity in CUBA-application is determined dynamically by the views mechanism.

  • optional – (optional parameter, true by default) – indicates whether the attribute can contain null value. If optional = false JPA ensures the existence of reference when the entity is saved. In addition, the visual components working with this attribute can request the user to enter a value.

For example, several Order instances refer to the same Customer instance. In this case the Order.customer attribute should have the following annotations:

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "CUSTOMER_ID")
protected Customer customer;
@MetaProperty

Indicates that metadata should include the annotated attribute. This annotation can be set for a field or for a getter method, if there is no corresponding field.

This annotation is not required for the fields already containing the following annotations from javax.persistence package: @Column, @OneToOne, @OneToMany, @ManyToOne, @ManyToMany, @Embedded. Such fields are included in metadata automatically. Thus, @MetaProperty is mainly used for defining non-persistent attributes of the entities.

Parameters (optional):

  • mandatory - determines whether the attribute can contain null value. If mandatory = true, visual components working with this attribute can request the user to enter a value.

  • datatype - explicitly defines a datatype that overrides a datatype inferred from the attribute Java type.

  • related - defines the array of related persistent attributes to be fetched from the database when this property is included in a view.

Field example:

@Transient
@MetaProperty
protected String token;

Method example:

@MetaProperty
public String getLocValue() {
    if (!StringUtils.isEmpty(messagesPack)) {
        return AppBeans.get(Messsages.class).getMessage(messagesPack, value);
    } else {
        return value;
    }
}
@OnDelete

Determines related entities handling policy in case of soft deletion of the entity, containing the attribute. See Soft Deletion.

Example:

@OneToMany(mappedBy = "group")
@OnDelete(DeletePolicy.CASCADE)
private Set<Constraint> constraints;
@OnDeleteInverse

Determines related entities handling policy in case of soft deletion of the entity from the inverse side of the relationship. See Soft Deletion.

Example:

@ManyToOne
@JoinColumn(name = "DRIVER_ID")
@OnDeleteInverse(DeletePolicy.DENY)
private Driver driver;
@OneToMany

Defines a collection attribute with one-to-many relationship type.

Parameters:

  • mappedBy – the field of the referenced entity, which owns the relationship.

  • targetEntity – the type of referenced entity. This parameter is optional if the collection is declared using Java generics.

  • fetch – (optional parameter, LAZY by default) – determines whether JPA will eagerly fetch the collection of referenced entities. This parameter should always remain LAZY, since retrieval of referenced entities in CUBA-application is determined dynamically by the views mechanism.

For example, several Item instances refer to the same Order instance using @ManyToOne field Item.order. In this case the Order class can contain a collection of Item instances:

@OneToMany(mappedBy = "order")
protected Set<Item> items;
@OneToOne

Defines a reference attribute with one-to-one relationship type.

Parameters:

  • fetch – (EAGER by default) determines whether JPA will eagerly fetch the referenced entity. This parameter should be set to LAZY, since retrieval of referenced entities in CUBA-application is determined dynamically by the views mechanism.

  • mappedBy – the field of the referenced entity, which owns the relationship. It must only be set on the non-owning side of the relationship.

  • optional – (optional parameter, true by default) – indicates whether the attribute can contain null value. If optional = false JPA ensures the existence of reference when the entity is saved. In addition, the visual components working with this attribute can request the user to enter a value.

Example of owning side of the relationship in the Driver class:

@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "CALLSIGN_ID")
protected DriverCallsign callsign;

Example of non-owning side of the relationship in the DriverCallsign class:

@OneToOne(fetch = FetchType.LAZY, mappedBy = "callsign")
protected Driver driver;
@OrderBy

Determines the order of elements in a collection attribute at the point when the association is retrieved from the database. This annotation should be specified for ordered Java collections such as List or LinkedHashSet to get a predictable sequence of elements.

Parameters:

  • value – string, determines the order in the format:

orderby_list::= orderby_item [,orderby_item]*
orderby_item::= property_or_field_name [ASC | DESC]

Example:

@OneToMany(mappedBy = "user")
@OrderBy("createTs")
protected List<UserRole> userRoles;
@Temporal

Specifies the type of the stored value for java.util.Date attribute: date, time or date+time.

Parameters:

  • value – the type of the stored value: DATE, TIME, TIMESTAMP

Example:

@Column(name = "START_DATE")
@Temporal(TemporalType.DATE)
protected Date startDate;
@Transient

Indicates that field is not stored in the database, meaning it is non-persistent.

The fields supported by JPA types (See http://docs.oracle.com/javaee/7/api/javax/persistence/Basic.html) are persistent by default, that is why @Transient annotation is mandatory for non-persistent attribute of such type.

@MetaProperty annotation is required if @Transient attribute should be included in metadata.

@Version

Indicates that the annotated field stores a version for optimistic locking support.

Such field is required when an entity class implements the Versioned interface (StandardEntity base class already contains such field).

Example:

@Version
@Column(name = "VERSION")
private Integer version;
5.2.1.3. Enum Attributes

The standard use of JPA for enum attributes involves an integer database field containing a value obtained from the ordinal() method. This approach may lead to the following issues with extending a system in production:

  • An entity instance cannot be loaded, if the value of the enum in the database does not equal to any ordinal value.

  • It is impossible to add a new value between the existing ones, which is important when sorting by enumeration value (order by).

CUBA-style approach to solving these problems is to detach the value stored in the database from ordinal value of the enumeration. In order to do this, the field of the entity should be declared with the type, stored in the database (Integer or String), while the access methods (getter / setter) should be created with the actual enumeration type.

Example:

@Entity(name = "sales$Customer")
@Table(name = "SALES_CUSTOMER")
public class Customer extends StandardEntity {

    @Column(name = "GRADE")
    protected Integer grade;

    public CustomerGrade getGrade() {
        return grade == null ? null : CustomerGrade.fromId(grade);
    }

    public void setGrade(CustomerGrade grade) {
        this.grade = grade == null ? null : grade.getId();
    }
    ...
}

In this case, the enumeration class can look like this:

public enum CustomerGrade implements EnumClass<Integer> {

    PREMIUM(10),
    HIGH(20),
    MEDIUM(30);

    private Integer id;

    CustomerGrade(Integer id) {
        this.id = id;
    }

    @Override
    public Integer getId() {
        return id;
    }

    public static CustomerGrade fromId(Integer id) {
        for (CustomerGrade grade : CustomerGrade.values()) {
            if (grade.getId().equals(id))
                return grade;
        }
        return null;
    }
}

For correct reflection in metadata, the enumeration class must implement the EnumClass interface.

As the examples show, grade attribute corresponds to the Integer type value stored in the database, which is specified by the id field of CustomerGrade enumeration, namely 10, 20 or 30. At the same time, the application code and metadata framework use CustomerGrade enum through access methods, which perform the actual conversion.

A call to getGrade() method will simply return null, if the value in the database does not correspond to any of the enumeration values. In order to add a new value, for example, HIGHER, between HIGH and PREMIUM, it is sufficient to add new enumeration value with id = 15, which ensures that sorting by Customer.grade field remains correct.

The Integer field type provides the ordered list of constants and enables sorting in JPQL and SQL queries (>, <, >=, , order by), not to mention the negligible issue of database space and performance. On the other hand, Integer values are not self-explanatory in query results, that complicates debugging and using raw data from the database or in serialized formats. In this regard, the String type is more convenient.

Enumerations can be created in CUBA Studio on the DATA MODEL tab (New → Enumeration). To be used as an entity attribute, choose ENUM in the Attribute type field of the attribute editor and select the Enumeration class in the Type field. Enumeration values can be associated with localized names that will be displayed in the user interface of the application.

5.2.1.4. Soft Deletion

CUBA platform supports soft deletion mode, when the records are not deleted from the database, but instead, marked in a special way, so that they become inaccessible for common use. Later, these records can be either completely removed from the database using some kind of scheduled procedure or restored.

Soft deletion mechanism is transparent for an application developer, the only requirement is for entity class to implement SoftDelete interface. The platform will adjust data operations automatically.

Soft deletion mode offers the following benefits:

  • Significantly reduces the risk of data loss caused by incorrect user actions.

  • Enables making certain records inaccessible instantly even if there are references to them.

    Using Orders-Customers data model as an example, let’s assume that a certain customer has made several orders but we need to make the customer instance inaccessible for users. This is impossible with traditional hard deletion, as deletion of a customer instance requires either deletion of all related orders or setting to null all references to the customer (meaning data loss). After soft deletion, the customer instance becomes unavailable for search and modification; however, a user can see the name of the customer in the order editor, as deletion attribute is purposely ignored when the related entities are fetched.

    The standard behavior above can be modified with related entities processing policy.

The deleted entity instances can be manually restored on the Restore Deleted Entities screen available from the Administration menu of an application. This functionality is designed only for application administrators supposed to have all permissions to all entities, and should be used carefully, so it is recommended to deny access to this screen for simple users.

The negative impact of soft deletion is increase in database size and likely need for additional cleanup procedures.

5.2.1.4.1. Use of Soft Deletion

To support soft deletion, the entity class should implement SoftDelete interface, and the corresponding database table should contain the following columns:

  • DELETE_TS – when the record was deleted.

  • DELETED_BY – the login of the user who deleted the record.

The default behavior for instances implementing SoftDelete interface, is that soft deleted entities are not returned by queries or search by id. If required, this behavior can by dynamically turned off using the following methods:

  • Calling setSoftDeletion(false) for the current EntityManager instance.

  • Calling setSoftDeletion(false) for LoadContext object when requesting data via DataManager.

  • On the datasource level – calling CollectionDatasource.setSoftDeletion(false) or setting softDeletion="false" attribute of collectionDatasource element in the screen’s XML-descriptor.

In soft deletion mode, the platform automatically filters out the deleted instances when loading by identifier and when using JPQL queries, as well as the deleted elements of the related entities in collection attributes. However, related entities in single-value (*ToOne) attributes are loaded, regardless of whether the related instance was deleted or not.

5.2.1.4.2. Related Entities Processing Policy

The platform offers a mechanism for managing related entities when deleting, which is largely similar to ON DELETE rules for database foreign keys. This mechanism works on the middle tier and uses @OnDelete, @OnDeleteInverse annotations on entity attributes.

@OnDelete annotation is processed when the entity in which this annotation is found is deleted, but not the one pointed to by this annotation (this is the main difference from cascade deletion at the database level).

@OnDeleteInverse annotation is processed when the entity which it points to is deleted (which is similar to cascade deletion at foreign key level in the database). This annotation is useful when the object being deleted has no attribute that can be checked before deletion. Typically, the object being checked has a reference to the object being deleted, and this is the attribute that should be annotated with @OnDeleteInverse.

Annotation value can be:

  • DeletePolicy.DENY – prohibits entity deletion, if the annotated attribute is not null or not an empty collection.

  • DeletePolicy.CASCADE – cascade deletion of the annotated attribute.

  • DeletePolicy.UNLINK – disconnect the link with the annotated attribute. It is reasonable to disconnect the link only in the owner side of the association – the one with @JoinColumn annotation in the entity class.

Examples:

  1. Prohibit deletion of entity with references: DeletePolicyException will be thrown if you try to delete Customer instance, which is referred to by at least one Order.

    Order.java

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "CUSTOMER_ID")
    @OnDeleteInverse(DeletePolicy.DENY)
    protected Customer customer;

    Customer.java

    @OneToMany(mappedBy = "customer")
    protected List<Order> orders;

    Messages in the exception window can be localized in the main message pack. Use the following keys:

    • deletePolicy.caption - notification caption.

    • deletePolicy.references.message - notification message.

    • deletePolicy.caption.sales$Customer - notification caption for concrete entity.

    • deletePolicy.references.message.sales$Customer - notification message for concrete entity.

  2. Cascade deletion of related collection elements: deletion of Role instance causes all Permission instances to be deleted as well.

    Role.java

    @OneToMany(mappedBy = "role")
    @OnDelete(DeletePolicy.CASCADE)
    protected Set<Permission> permissions;

    Permission.java

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "ROLE_ID")
    protected Role role;
  3. Disconnect the links with related collection elements: deletion of Role instance leads to setting to null references to this Role for all Permission instances included in the collection.

    Role.java

    @OneToMany(mappedBy = "role")
    protected Set<Permission> permissions;

    Permission.java

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "ROLE_ID")
    @OnDeleteInverse(DeletePolicy.UNLINK)
    protected Role role;

Implementation notes:

  1. Related entities policy is processed on Middleware when saving entities to the database.

  2. Be careful when using @OnDeleteInverse together with CASCADE and UNLINK policies. During this process, all instances of the related objects are fetched from the database, modified and then saved.

    For example, if @OnDeleteInverse(CASCADE) policy is set on Job.customer attribute in a CustomerJob association with many jobs to one customer, if you set @OnDeleteInverse(CASCADE) policy on Job.customer attribute, all jobs will be retrieved and modified when deleting a Customer instance. This may overload the application server or the database.

    On the other hand, using @OnDeleteInverse(DENY) is safe, as it only involves counting the number of the related objects. If there are more than 0, an exception is thrown. This makes use of @OnDeleteInverse(DENY) suitable for Job.customer attribute.

  3. The UNLINK policy for one-to-many and many-to-many collection attributes is not supported: UnsupportedOperationException will be thrown if you try to delete an entity instance on the owner side of the association. For example, the following delete policy will not work:

    Owner.java

    @JoinTable(name = "SAMPLE_OWNER_SUBORDINATE_LINK",
        joinColumns = @JoinColumn(name = "OWNER_ID"),
        inverseJoinColumns = @JoinColumn(name = "SUBORDINATE_ID"))
    @OnDelete(DeletePolicy.UNLINK)
    @ManyToMany
    protected List<Subordinate> subordinate;
5.2.1.4.3. Unique Constraints at Database Level

In order to apply unique constraints for certain value in the soft deletion mode, at least one non-deleted record with this value and an arbitrary number of deleted records with the same value may exist in database.

This logic can be implemented in a specific way for each database server type:

  • If database server supports partial indexes (e.g. PostgreSQL), unique restrictions can be achieved as follows:

    create unique index IDX_SEC_USER_UNIQ_LOGIN on SEC_USER (LOGIN_LC) where DELETE_TS is null
  • If database server does not support partial indexes (e.g. Microsoft SQL Server 2005), DELETE_TS field can be included in the unique index:

    create unique index IDX_SEC_USER_UNIQ_LOGIN on SEC_USER (LOGIN_LC, DELETE_TS)

5.2.2. Metadata Framework

Metadata framework is used to support efficient work with data model in CUBA-applications. The framework:

  • provides API for obtaining information about entities, their attributes and relations between the entities; it is also used for traversing object graphs;

  • serves as a specialized and more convenient alternative for Java Reflection API;

  • controls permitted data types and relationships between entities;

  • enables implementation of universal mechanisms for operations with data.

5.2.2.1. Metadata Interfaces

Let’s consider the basic metadata interfaces.

MetadataFramework
Figure 9. Metadata Framework Interfaces
Session

Entry point of the metadata framework. Enables obtaining MetaClass instances by name and by the corresponding Java class. Note the difference in methods: getClass() methods can return null while getClassNN() (Non Null) methods cannot.

Session object can be obtained using the Metadata infrastructure interface.

Example:

@Inject
protected Metadata metadata;
...
Session session = metadata.getSession();
MetaClass metaClass1 = session.getClassNN("sec$User");
MetaClass metaClass2 = session.getClassNN(User.class);
assert metaClass1 == metaClass2;
MetaModel

Rarely used interface intended to group meta-classes.

Meta-classes are grouped by the root name of Java project package specified in metadata.xml file.

MetaClass

Entity class metadata interface. MetaClass is always associated with the Java class which it represents.

Basic methods:

  • getName() – entity name, according to convention the first part of the name before $ sign is the namespace code, for example, sales$Customer.

  • getProperties() – the list of meta-properties (MetaProperty).

  • getProperty(), getPropertyNN() – methods return meta-properties by name. If there is no attribute with provided name, the first method returns null, and the second throws an exception.

    Example:

    MetaClass userClass = session.getClassNN(User.class);
    MetaProperty groupProperty = userClass.getPropertyNN("group");
  • getPropertyPath() – allows you to navigate by references. This method accepts string parameter – path in the format of dot-separated attribute names. The returned MetaPropertyPath object enables accessing the required (the last in the path) attribute by invoking getMetaProperty() method.

    Example:

    MetaClass userClass = session.getClassNN(User.class);
    MetaProperty groupNameProp = userClass.getPropertyPath("group.name").getMetaProperty();
    assert groupNameProp.getDomain().getName().equals("sec$Group");
  • getJavaClass() – entity class, corresponding to this MetaClass.

  • getAnnotations() – collection of meta-annotations.

MetaProperty

Entity attribute metadata interface.

Basic methods:

  • getName() – property name, corresponds to entity attribute name.

  • getDomain() – meta-class, owning this property.

  • getType()- the property type:

    • simple type: DATATYPE

    • enumeration: ENUM

    • reference type of two kinds:

      • ASSOCIATION − simple reference to another entity. For example, Order-Customer relationship is an association.

      • COMPOSITION − reference to the entity, having no consistent value without the owning entity. COMPOSITION is considered to be a "closer" relationship than ASSOCIATION. For example, the relationship between Order and its Items is a COMPOSITION, as the Item cannot exist without the Order to which it belongs.

        The type of ASSOCIATION or COMPOSITION reference attributes affects entity edit mode: in the first case the related entity is persisted to the database independently, in the second case – only together with the owning entity. See Composite Structures for details.

  • getRange()Range interface providing detailed description of the attribute type.

  • isMandatory() – indicates a mandatory attribute. For instance, it is used by visual components to signal a user that value is mandatory.

  • isReadOnly() – indicates a read-only attribute.

  • getInverse() – for reference-type attribute, returns the meta-property from the other side of the association, if such exists.

  • getAnnotatedElement() – field (java.lang.reflect.Field) or method (java.lang.reflect.Method), corresponding to the entity attribute.

  • getJavaType() – Java class of the entity attribute. It can either be the type of corresponding field or the type of the value returned by corresponding method.

  • getDeclaringClass() – Java class containing this attribute.

    Range

    Interface describing entity attribute type in detail.

    Basic methods:

  • isDatatype() – returns true for simple type attribute.

  • asDatatype() – returns Datatype for simple type attribute.

  • isEnum() – returns true for enumeration type attribute.

  • asEnumeration() – returns Enumeration for enumeration type attribute.

  • isClass() – returns true for reference attribute of ASSOCIATION or COMPOSITION type.

  • asClass() – returns metaclass of associated entity for a reference attribute.

  • isOrdered() – returns true if the attribute is represented by an ordered collection (for example List).

  • getCardinality() – relation kind of the reference attribute: ONE_TO_ONE, MANY_TO_ONE, ONE_TO_MANY, MANY_TO_MANY.

5.2.2.2. Metadata Building

The main source for metadata structure generation are annotated entity classes.

Entity class will be present in the metadata in the following cases:

  • Persistent entity class is annotated by @Entity, @Embeddable, @MappedSuperclass and is located within the root package specified in metadata.xml.

  • Non-persistent entity class is annotated by @MetaClass and is located within the root package specified in metadata.xml.

All entities inside same root package are put into the same MetaModel instance, which is given the name of this package. Entities within the same MetaModel can contain arbitrary references to each other. References between entities from different meta-models can be created in the order of declaration of metadata.xml files in cuba.metadataConfig property.

Entity attribute will be present in metadata if:

  • A class field is annotated by @Column, @OneToOne, @OneToMany, @ManyToOne, @ManyToMany, @Embedded.

  • A class field or an access method (getter) is annotated by @MetaProperty.

Metaclass and metaproperty parameters are determined on the base of the listed annotations parameters as well as field types and class methods. Besides, if an attribute does not have write access method (setter), it becomes immutable (read only).

5.2.2.3. Datatype

Datatype interface describes a valid data type for an entity attribute if it is not a reference.

Datatypes are registered in the Datatypes class, which loads and initializes Datatype implementation classes in the following way:

  • Default Datatypes implementations are loaded from the /com/haulmont/chile/core/datatypes/datatypes.xml classpath resource.

  • metadata.xml files from application components and from the project itself are scanned for the datatypes element and if it is found, custom Datatype implementations are loaded. See an example of a custom datatype below.

  • For backward compatibility, the platform searches for the datatypes.xml file in CLASSPATH root, and if it is found, default Datatype implementations are loaded from it instead of the resource mentioned above.

Datatype instance can be obtained in two ways:

  • For an entity attribute – from the corresponding meta-property using getRange().asDatatype() call.

  • Using Datatypes.get() static method by passing to it the name of the Datatype implementation or Java class it was created for.

Datatypes are associated with entity attributes during application start according to the following rules:

  • If @MetaProperty annotation is defined on the field or method having a non-empty datatype value, the attribute is associated with the Datatype instance with the given name.

    For instance, if the entity attribute is declared as in the example below, it will be associated with a custom type – GeoCoordinateDatatype:

    @MetaProperty(datatype = GeoCoordinateDatatype.NAME)
    @Column(name = "LATITUDE")
    private Double latitude;
  • In most cases, explicit specification is omitted, and the attribute is associated with the Datatype instance from repository, which is returned by Datatypes.get(Class) by supplied field or method type.

    In the example below, latitude attribute will get a standard DoubleDatatype type registered in the /com/haulmont/chile/core/datatypes/datatypes.xml resource:

    @Column(name = "LATITUDE")
    private Double latitude;

Basic methods of Datatype interfaces:

  • getName() – returns the unique name of the implementation.

  • format() – converts the passed value into a string.

  • parse() – transforms a string into the value of corresponding type.

Datatype determines two sets of methods for formatting and parsing: considering and not considering locale. Conversion considering locale is applied everywhere in user interface, ignoring locale – in system mechanisms, for example, serialization in REST API.

Parsing formats ignoring locale are specified in the above mentioned datatypes.xml resource or in the datatypes element of the metadata.xml file.

The parsing formats considering locale are provided in the main messages pack, in the strings with the following keys:

  • numberDecimalSeparator – specifies decimal separator for numeric types.

  • numberGroupingSeparator – defines separator between digits groups for numeric types (e.g. when space is used as separator, number will be formatted as 1 000 000).

  • integerFormat – format for Integer and Long types.

  • doubleFormat – format for Double type.

  • decimalFormat – format for `BigDecimal `type.

  • dateTimeFormat – format for java.util.Date type.

  • dateFormat – format for java.sql.Date type.

  • timeFormat – format for java.sql.Time type.

  • trueString – string corresponding to Boolean.TRUE.

  • falseString – string corresponding to Boolean.FALSE.

All the listed formats are specified in the main message pack of CUBA application component by default, and can be overridden in the similar files of the application project.

5.2.2.3.1. Example of Data Formatting in UI

Let us consider the way Order.date attribute is displayed in orders browser table.

order-browse.xml

<table id="ordersTable">
    ...
    <columns>
        <column id="date"/>
        ...

date attribute in Order class is defined using "date" type:

@Column(name = "DATE", nullable = false)
@Temporal(TemporalType.DATE)
private Date date;

If the current user is logged in with the Russian locale, the following string is retrieved from the main message pack on the client tier:

dateFormat=dd.MM.yyyy

The result: date "6th August 2012 " is converted into a string "06.08.2012" which is displayed in the table cell.

5.2.2.3.2. Examples of Date and Number Formatting in the Application Code

If you need to format or parse values of BigDecimal, Integer, Long, Double, Boolean or Date types depending on the current user locale, use the DatatypeFormatter bean. For example:

@Inject
private DatatypeFormatter formatter;
...
String localDate = formatter.formatDate(dateField.getValue());

Below are examples of using Datatype methods directly.

  • Date formatting example

    @Inject
    protected UserSessionSource userSessionSource;
    ...
    Date date = ...;
    String dateStr = Datatypes.getNN(Date.class).format(date, userSessionSource.getLocale());
  • Example of formatting of numeric values with high accuracy (up to 5 decimal places) in Web Client:

    /com/sample/sales/web/messages_ru.properties

    coordinateFormat = #,##0.00000

    SomeClass.java

    @Inject
    protected Messages messages;
    @Inject
    protected UserSessionSource userSessionSource;
    ...
    String coordinateFormat = messages.getMainMessage("coordinateFormat");
    FormatStrings formatStrings = Datatypes.getFormatStrings(userSessionSource.getLocale());
    NumberFormat format = new DecimalFormat(coordinateFormat, formatStrings.getFormatSymbols());
    
    String formattedValue = format.format(value);
5.2.2.3.3. Example of a Custom Datatype

Let us consider the implementation of a custom GeoCoordinateDatatype, intended for the attributes storing geographical coordinates.

First, create the following class in the global module:

package com.company.sample;

import com.haulmont.chile.core.datatypes.Datatypes;
import com.haulmont.chile.core.datatypes.FormatStrings;
import com.haulmont.chile.core.datatypes.impl.DoubleDatatype;
import org.apache.commons.lang.StringUtils;
import org.dom4j.Element;

import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Locale;

public class GeoCoordinateDatatype extends DoubleDatatype {

    // This field is required for Studio even if you don't use it in code
    public static final String NAME = "geocoordinate";

    // The format is the same for all locales but may differ in decimal points
    public static final String FORMAT = "#0.000000";

    public GeoCoordinateDatatype(Element element) {
        super(element);
    }

    @Override
    public String getName() {
        return NAME;
    }

    @Override
    public String format(Object value, Locale locale) {
        if (value == null)
            return "";
        FormatStrings formatStrings = Datatypes.getFormatStrings(locale);
        if (formatStrings == null)
            return format(value);

        NumberFormat format = new DecimalFormat(FORMAT, formatStrings.getFormatSymbols());
        return format.format(value);
    }

    @Override
    public Double parse(String value, Locale locale) throws ParseException {
        if (StringUtils.isBlank(value))
            return null;
        FormatStrings formatStrings = Datatypes.getFormatStrings(locale);
        if (formatStrings == null)
            return parse(value);

        NumberFormat format = new DecimalFormat(FORMAT, formatStrings.getFormatSymbols());
        return parse(value, format).doubleValue();
    }
}

Then add the datatypes element to the metadata.xml of your project:

<metadata xmlns="http://schemas.haulmont.com/cuba/metadata.xsd">
    <datatypes>
        <datatype class="com.company.sample.GeoCoordinateDatatype"
                  format="#0.000000" decimalSeparator="." groupingSeparator=""/>
    </datatypes>

    <!-- ... -->
</metadata>

In the datatype element, you can also specify the sqlType attribute containing an SQL type of your database suitable for storing values of the new type. This SQL type will be used by CUBA Studio when it generates database scripts. Studio can automatically determine an SQL type for the following Java types:

  • java.lang.Boolean

  • java.lang.Integer

  • java.lang.Long

  • java.math.BigDecimal

  • java.lang.Double

  • java.lang.String

  • java.util.Date

  • java.util.UUID

  • byte[]

Specify one of the listed Java types in the javaType attribute to make this datatype available in the Studio entity attribute editor. In this case, the sqlType attribute can be omitted.

Finally, specify the new datatype for the required attributes (programmatically or with the help of Studio):

@MetaProperty(datatype = GeoCoordinateDatatype.NAME)
@Column(name = "LATITUDE")
private Double latitude;

After the above listed operations are completed, the latitude attribute will be displayed in the desired format throughout the application.

5.2.2.4. Meta-Annotations

Entity meta-annotations are a set of key/value pairs providing additional information about entities.

Meta-annotations are accessed using meta-class getAnnotations() method.

The sources of meta-annotations are:

  • @OnDelete, @OnDeleteInverse, @Extends annotations. These annotations cause creation of special meta-annotations for describing relations between entities.

  • Extendable meta-annotations marked with @MetaAnnotation. These annotations are converted to meta-annotations with a key corresponding to the full name of Java class of the annotation and a value which is a map of annotation attributes. For example, @TrackEditScreenHistory annotation will have a value which is a map with a single entry: value → true. The platform provides the following annotations of this kind: @NamePattern, @SystemLevel, @EnableRestore, @TrackEditScreenHistory. In your application or application components, you can create your own annotation classes and mark them with @MetaAnnotation annotation.

  • Optional: entity meta-annotations can also be defined in metadata.xml files. If a meta-annotation in XML has the same name as the meta-annotation created by Java entity class annotation, then it will override the latter.

    The example below shows how to override meta-annotations in metadata.xml:

    <metadata xmlns="http://schemas.haulmont.com/cuba/metadata.xsd">
        <!-- ... -->
    
        <annotations>
            <entity class="com.company.customers.entity.Customer">
                <annotation name="com.haulmont.cuba.core.entity.annotation.TrackEditScreenHistory">
                    <attribute name="value" value="true" datatype="boolean"/>
                </annotation>
    
                <property name="name">
                    <annotation name="length" value="200"/>
                </property>
    
                <property name="customerGroup">
                    <annotation name="com.haulmont.cuba.core.entity.annotation.Lookup">
                        <attribute name="type" class="com.haulmont.cuba.core.entity.annotation.LookupType" value="DROPDOWN"/>
                        <attribute name="actions" datatype="string">
                            <value>lookup</value>
                            <value>open</value>
                        </attribute>
                    </annotation>
                </property>
            </entity>
    
            <entity class="com.company.customers.entity.CustomerGroup">
                <annotation name="com.haulmont.cuba.core.entity.annotation.EnableRestore">
                    <attribute name="value" value="false" datatype="boolean"/>
                </annotation>
            </entity>
        </annotations>
    </metadata>

5.2.3. Views

When retrieving entities from the database, we often face the question: how to ensure loading of related entities to the desired depth?

For example, you need to display the date and amount together with the Customer name in the Orders browser, which means that you need to fetch the related Customer instance. And for the Order editor screen, you need to fetch the collection of Items, in addition to that each Item should contain a related Product instance to display its name.

Lazy loading can not help in most cases because data processing is usually performed not in the transaction where the entities were loaded but, for example, on the client tier in UI. At the same time, it is unacceptable to apply eager fetching using entity annotations as it leads to constant retrieval of the entire graph of related entities which can be very large.

Another similar problem is the requirement to limit the set of local entity attributes of the loaded graph: for example, some entity can have 50 attributes, including BLOB, but only 10 attributes need to be displayed on the screen. In this case, why should we download 40 remaining attributes from the database, then serialize them and transfer to the client when it does not need them at the moment?

Views mechanism resolves these issues by retrieving from the database and transmitting to the client entity graphs limited by depth and by attributes. A view is a descriptor of the object graph required for a certain UI screen or data-processing operation.

Views processing is performed in the following way:

  • All relations in the data model are declared with lazy fetching property (fetch = FetchType.LAZY. See Entity Annotations).

  • In the data loading process, the calling code provides required view together with JPQL query or entity identifier.

  • The so-called FetchGroup is produced on the base of the view – this is a special feature of EclipseLink framework lying in the base of the ORM layer. FetchGroup affects the generation of SQL queries to the database: both the list of returned fields and joins with other tables containing related entities.

View
Figure 10. View Classes

A view is determined by an instance of the View class, where:

  • entityClass – the entity class, for which the view is defined. In other words, it is the "root" of the loaded entities tree.

  • name – the name of the view. It should be either null or a unique name within all views for the entity.

  • properties – collection of ViewProperty instances corresponding to the entity attributes that should be loaded.

  • includeSystemProperties – if set, system attributes (defined by basic interfaces of persistent entities, such as BaseEntity and Updatable) are automatically included in the view.

ViewProperty class has the following properties:

  • name – the name of the entity attribute.

  • view – for reference attributes, specifies the view which which will be used to load the related entity.

  • fetch - for reference attributes, specifies how to fetch the related entity from the database. It corresponds to the FetchMode enum and can have one of the following values:

    • AUTO - the platform will choose an optimal mode depending on the relation type.

    • UNDEFINED - fetching will be performed according to JPA rules, which effectively means loading by a separate select.

    • JOIN - fetching in the same select by joining with referenced table.

    • BATCH - queries of related objects will be optimized in batches. See more here.

    If the fetch attribute is not specified, the AUTO mode is applied. If the reference represents a cacheable entity, UNDEFINED will be used regardless of the value specified in the view.

Tip

Regardless of the attributes defined in the view, the following attributes are always loaded:

  • id – entity identifier.

  • version – used for optimistic locking of the entities implementing Versioned.

  • deleteTs, deletedBy – used for the entities, implementing SoftDelete.

Warning

An attempt to get or set a value for a not loaded attribute (not included into a view) raises an exception. You can check whether the attribute was loaded using the PersistenceHelper.isLoaded() method.

5.2.3.1. Views Creation

A view can be created in two possible ways:

  • Programmatically – by creating a View instance, for example:

    View view = new View(Order.class)
            .addProperty("date")
            .addProperty("amount")
            .addProperty("customer", new View(Customer.class)
                .addProperty("name")
            );

    Typically, this way can be appropriate for creating views that are used in a single piece of business logic.

  • Declaratively – by creating an XML descriptor and deploying it to ViewRepository. View instances are created and cached when the XML descriptor is deployed. Further on, the required view can be retrieved in any part of the application code by a call to ViewRepository providing the entity class and the view name.

Let us consider in details the declarative way for creation and working with views.

ViewRepository is a Spring bean, accessible to all application blocks. The reference to ViewRepository can be obtained using injection or through the Metadata infrastructure interface. ViewRepository.getView() methods are used to retrieve view instances from the repository. deployViews() methods from AbstractViewRepository basic implementation are used to deploy XML descriptors to the repository.

Two views named _local and _minimal are available in the views repository for each entity by default:

  • _local contains all local entity attributes.

  • _minimal contains the attributes which are included to the name of the entity instance and specified in the @NamePattern annotation. If the @NamePattern annotation is not specified at the entity, this view does not contain any attributes.

The detailed structure of view XML descriptors is explained here.

The example below shows a view descriptor for the Order entity which provides loading of all local attributes, associated Customer and the Items collection:

<view class="com.sample.sales.entity.Order"
      name="orderWithCustomer"
      extends="_local">
    <property name="customer" view="_minimal"/>
    <property name="items" view="itemInOrder"/>
</view>

The recommended way of grouping and deployment of view descriptors is as follows:

  • Create views.xml file in the src root of the global module and place all view descriptors that should be globally accessible (i.e. on all application tiers) into it.

  • Register this file in the [cuba.viewsConfig] application property of all blocks, i.e. in app.properties of the core module, web-app.properties of the web module, etc. This will ensure automatic deployment of the views upon application startup into the repository.

  • If there are views which are used only in one application block, they can be specified in the similar file of this block, for example, web-views.xml, and registered in [cuba.viewsConfig] property of this block only.

    If the repository contains a view with certain name for some entity, an attempt to deploy another view with this name for the same entity will be ignored. If you need to replace the existing view in the repository with a new one and guarantee its deployment, specify overwrite = "true" attribute for it.

Tip

It is recommended to give descriptive names to the views. For example, not just "browse", but "customerBrowse". It simplifies the search of views in XML descriptors.

5.2.4. Managed Beans

Managed Beans are program components intended for implementation of the application’s business logic. "Managed" in this case means that the instance creation and dependency management is handled by the container, which is the main part of the Spring framework.

Tip

Managed Bean is a singleton by default, i.e., only one instance of such class exists in each application block. If a singleton bean contains mutable data in fields (in other words, has a state), it is necessary to synchronize access to such data.

5.2.4.1. Creating a Bean

To create a managed bean, add the @org.springframework.stereotype.Component annotation to the Java class. For example:

package com.sample.sales.core;

import com.sample.sales.entity.Order;
import org.springframework.stereotype.Component;

@Component(OrderWorker.NAME)
public class OrderWorker {
    public static final String NAME = "sales_OrderWorker";

    public void calculateTotals(Order order) {
    }
}

It is recommended to assign a unique name to the bean in the {project_name}_{class_name} form and to define it in the NAME constant.

Tip

The @javax.annotation.ManagedBean can also be used for the managed bean definition, but it can cause problems when deploying the application into some application servers. Therefore we recommend to use only @Component annotation from Spring Framework.

The managed bean class should be placed inside the package tree with the root specified in the context:component-scan element of the spring.xml file. In this case, the spring.xml file contains the element:

<context:component-scan base-package="com.sample.sales"/>

which means that the search for annotated beans for this application block will be performed starting with the com.sample.sales package.

Managed beans can be created on any tier, because the Spring Framework container is used in all standard blocks of the application.

5.2.4.2. Using the Bean

A reference to the bean can be obtained through injection or through the AppBeans class. As an example of using the bean, let us look at the implementation of the OrderService bean that delegates the execution to the OrderWorker bean:

package com.sample.sales.core;

import com.haulmont.cuba.core.Persistence;
import com.sample.sales.entity.Order;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.inject.Inject;

@Service(OrderService.NAME)
public class OrderServiceBean implements OrderService {

    @Inject
    protected Persistence persistence;

    @Inject
    protected OrderWorker orderWorker;

    @Transactional
    @Override
    public BigDecimal calculateTotals(Order order) {
        Order entity = persistence.getEntityManager().merge(order);
        orderWorker.calculateTotals(entity);
    }
}

In this example, the service starts a transaction, merges the detached entity obtained from the client level into the persistent context, and passes the control to the OrderWorker bean, which contains the main business logic.

5.2.5. JMX Beans

Sometimes, it is necessary to give system administrator an ability to view and change the state of some managed bean at runtime. In such case, it is recommended to create a JMX bean – a program component having the JMX interface. JMX bean is usually a wrapper delegating calls to the managed bean which actually maintains state: cache, configuration data or statistics.

JMXBeans
Figure 11. JMX Bean Class Diagram

As you can see from the diagram, the JMX bean consists of the interface and the implementation class. The class should be a managed bean, i.e., should have the @Component annotation and unique name. The interface of the JMX bean is registered in spring.xml in a special way to create the JMX interface in the current JVM.

Calls to all JMX bean interface methods are intercepted using Spring AOP by the MBeanInterceptor interceptor class, which sets the correct ClassLoader in the current thread and enables logging of unhandled exceptions.

Warning

The JMX bean interface name must conform to the following format: {class_name}MBean.

JMX-interface can be utilized by external tools, such as jconsole or jvisualvm. In addition, the Web Client platform block includes the JMX console, which provides the basic tools to view the status and call the methods of the JMX beans.

5.2.5.1. Creating a JMX Bean

The following example shows how to create a JMX bean.

  • JMX bean interface:

    package com.sample.sales.core;
    
    import org.springframework.jmx.export.annotation.*;
    
    @ManagedResource(description = "Performs operations on Orders")
    public interface OrdersMBean {
    
        @ManagedOperation(description = "Recalculates an order amount")
        @ManagedOperationParameters({@ManagedOperationParameter(name = "orderId", description = "")})
        String calculateTotals(String orderId);
    }
    • The interface and its methods may contain annotations to specify the description of the JMX bean and its operations. This description will be displayed in all tools that work with this JMX interface, thereby helping the system administrator.

    • Optional @JmxRunAsync annotation is designed to denote long operations. When such operation is launched using the built-in JMX console, the platform displays a dialog with an indefinite progress bar and the Cancel button. A user can abort the operation and continue to work with the application. The annotation can also contain the timeout parameter that sets a maximum execution time for the operation in milliseconds, for example:

      @JmxRunAsync(timeout = 30000)
      String calculateTotals();

      If the timeout is exceeded, the dialog closes with an error message.

      Warning

      Please note, that if an operation is cancelled or timed out on UI, it still continue to work in background, i.e. these actions do not abort the actual execution, they just return control back to the user.

    • Since the JMX tools support a limited set of data types, it is desirable to use String as the type for the parameters and result of the method and perform the conversion inside the method, if necessary. Alongside with String, the following parameter types are supported: boolean, double, float, int, long, Boolean, Integer.

  • The JMX bean class:

    package com.sample.sales.core;
    
    import com.haulmont.cuba.core.*;
    import com.haulmont.cuba.core.app.*;
    import com.sample.sales.entity.Order;
    import org.apache.commons.lang.exception.ExceptionUtils;
    import org.springframework.stereotype.Component;
    import javax.inject.Inject;
    import java.util.UUID;
    
    @Component("sales_OrdersMBean")
    public class Orders implements OrdersMBean {
    
        @Inject
        protected OrderWorker orderWorker;
    
        @Inject
        protected Persistence persistence;
    
        @Authenticated
        @Override
        public String calculateTotals(String orderId) {
            try {
                try (Transaction tx = persistence.createTransaction()) {
                    Order entity = persistence.getEntityManager().find(Order.class, UUID.fromString(orderId));
                    orderWorker.calculateTotals(entity);
                    tx.commit();
                };
                return "Done";
            } catch (Throwable e) {
                return ExceptionUtils.getStackTrace(e);
            }
        }
    }

    The @Component annotation defines the class as a managed bean with the sales_OrdersMBean name. The name is specified directly in the annotation and not in the constant, since access to the JMX bean from Java code is not required.

    Lets overview the implementation of the calculateTotals() method.

    • The method has the @Authenticated annotation, i.e., system authentication is performed on method entry in the absence of the user session.

    • The method’s body is wrapped in the try/catch block, so that, if successful, the method returns "Done", and in case of error – the stack trace of the exception as string.

      In this case, all exceptions are handled and therefore do not get logged automatically, because they never fall through to MBeanInterceptor. If logging of exceptions is required, the call of the logger should be added in the catch section.

    • The method starts the transaction, loads the Order entity instance by identifier, and passes control to the OrderWorker bean for processing.

  • The registration of the JMX bean in spring.xml:

    <bean id="sales_MBeanExporter" lazy-init="false"
          class="com.haulmont.cuba.core.sys.jmx.MBeanExporter">
        <property name="beans">
            <map>
                <entry key="${cuba.webContextName}.sales:type=Orders"
                       value-ref="sales_OrdersMBean"/>
            </map>
        </property>
    </bean>

    All JMX beans of a project are declared in one MBeanExporter instance in the map/entry elements of the beans property. The key is JMX ObjectName, the value – the bean’s name specified in the @Component annotation. ObjectName begins with the name of the web application, because several web applications, which export the same JMX interfaces, can be deployed into one application server instance (i.e., into one JVM).

5.2.5.2. The Platform JMX Beans

This section describes some of the JMX beans available in the platform.

5.2.5.2.1. CachingFacadeMBean

CachingFacadeMBean provides methods to clear various caches in the Middleware and Web Client blocks.

JMX ObjectName: app-core.cuba:type=CachingFacade and app.cuba:type=CachingFacade

5.2.5.2.2. ConfigStorageMBean

ConfigStorageMBean enables viewing and setting values of the application properties in the Middleware, Web Client and Web Portal blocks.

This interface has separate sets of operations for working with properties stored in files (*AppProperties) and stored in the database (*DbProperties). These operations show only the properties explicitly set in the storage. It means that if you have a configuration interface defining a property and its default value, but you did not set the value in the database (or a file), these methods will not show the property and its current value.

Please note that the changes to property values stored in files are not persistent, and are valid only until restart of the application block.

Unlike the operations described above, the getConfigValue() operation returns exactly the same value as the corresponding method of the configuration interface invoked in the application code.

JMX ObjectName:

  • app-core.cuba:type=ConfigStorage

  • app.cuba:type=ConfigStorage

  • app-portal.cuba:type=ConfigStorage

5.2.5.2.3. EmailerMBean

EmailerMBean enables viewing the current values of the email sending parameters, and sending test messages.

JMX ObjectName: app-core.cuba:type=Emailer

5.2.5.2.4. PersistenceManagerMBean

PersistenceManagerMBean provides the following abilities:

  • Managing entity statistics mechanism.

  • Viewing new DB update scripts using the findUpdateDatabaseScripts() method. Triggering DB update with the updateDatabase() method.

  • Executing arbitrary JPQL queries in the Middleware context by using jpqlLoadList(), jpqlExecuteUpdate() methods.

JMX ObjectName: app-core.cuba:type=PersistenceManager

5.2.5.2.5. ScriptingManagerMBean

ScriptingManagerMBean is the JMX facade for the Scripting infrastructure interface.

JMX ObjectName: app-core.cuba:type=ScriptingManager

JMX attributes:

JMX operations:

  • runGroovyScript() – executes a Groovy script in the Middleware context and returns the result. The following variables are passed to the script:

    • persistence of the Persistence type.

    • metadata of the Metadata type.

    • configuration of the Configuration type.

    • dataManager of the DataManager type.

      The result type should be of the String type to be displayed in the JMX interface. Otherwise, the method is similar to the Scripting.runGroovyScript() method.

      The example script for creating a set of test users is shown below:

      import com.haulmont.cuba.core.*
      import com.haulmont.cuba.core.global.*
      import com.haulmont.cuba.security.entity.*
      
      PasswordEncryption passwordEncryption = AppBeans.get(PasswordEncryption.class)
      
      Transaction tx = persistence.createTransaction()
      try {
          EntityManager em = persistence.getEntityManager()
          Group group = em.getReference(Group.class, UUID.fromString('0fa2b1a5-1d68-4d69-9fbd-dff348347f93'))
          for (i in (1..250)) {
              User user = new User()
              user.setGroup(group)
              user.setLogin("user_${i.toString().padLeft(3, '0')}")
              user.setName(user.login)
              user.setPassword(passwordEncryption.getPasswordHash(user.id, '1'));
              em.persist(user)
          }
          tx.commit()
      } finally {
          tx.end()
      }
5.2.5.2.6. ServerInfoMBean

ServerInfoMBean provides the general information about this Middleware block: the build number, build date and the server id.

JMX ObjectName: app-core.cuba:type=ServerInfo

5.2.6. Infrastructure Interfaces

Infrastructure interfaces provide access to frequently used functionality of the platform. Most of them are located in the global module and can be used both on the middle and client tiers. However, some of them (Persistence, for example) are accessible only for Middleware code.

Infrastructure interfaces are implemented by Spring Framework beans, so they can be injected into any other managed components (managed beans, Middleware services, generic user interface screen controllers).

Also, like any other beans, infrastructure interfaces can be obtained using static methods of AppBeans class, and can be used in non-managed components (POJO, helper classes etc.).

5.2.6.1. Configuration

The interface helps to obtain references to configuration interfaces.

Examples:

// field injection

@Inject
protected Configuration configuration;
...
String tempDir = configuration.getConfig(GlobalConfig.class).getTempDir();
// setter injection

protected GlobalConfig globalConfig;

@Inject
public void setConfiguration(Configuration configuration) {
    this.globalConfig = configuration.getConfig(GlobalConfig.class);
}
// location

String tempDir = AppBeans.get(Configuration.class).getConfig(GlobalConfig.class).getTempDir();
5.2.6.2. Messages

Messages interface provides methods to get localized message strings.

Let’s consider interface methods in detail.

  • getMessage() – returns the localized message by key, pack name and required locale. There are several modifications of this method with different sets of parameters. If locale is not specified in the method parameter, the current user locale is used.

    Examples:

    @Inject
    protected Messages messages;
    ...
    String message1 = messages.getMessage(getClass(), "someMessage");
    String message2 = messages.getMessage("com.abc.sales.web.customer", "someMessage");
    String message3 = messages.getMessage(RoleType.STANDARD);
  • formatMessage() – retrieves a localized message by key, pack name and required locale, then uses it to format the input parameters. The format is defined according to String.format() method rules. There are several modifications of this method with different sets of parameters. If locale is not specified in the method parameter, the current user locale is used.

    Example:

    String formattedValue = messages.formatMessage(getClass(), "someFormat", someValue);
  • getMainMessage() – returns the localized message from the main message pack of the application block.

    Example:

    protected Messages messages = AppBeans.get(Messages.class);
    ...
    messages.getMainMessage("actions.Ok");
  • getMainMessagePack() – returns the name of the main message pack of the application block.

    Example:

    String formattedValue = messages.formatMessage(messages.getMainMessagePack(), "someFormat", someValue);
  • getTools() – returns MessageTools interface instance (see below).

5.2.6.2.1. MessageTools

MessageTools interface is a managed bean containing additional methods for working with localized messages. You can access MessageTools interface either using Messages.getTools() method, or as any other bean – by means of injection or through AppBeans class.

MessageTools methods:

  • loadString() – returns a localized message, specified by reference in msg://{messagePack}/{key} format

    Reference components:

    • msg:// – mandatory prefix.

    • {messagePack} – optional name of the message pack. If it is not specified, it is assumed that the pack name is passed to loadString() as a separate parameter.

    • {key} – message key in the pack.

    Examples of the message references:

    msg://someMessage
    msg://com.abc.sales.web.customer/someMessage
  • getEntityCaption() – returns the localized entity name.

  • getPropertyCaption() – returns the localized name of an entity attribute.

  • hasPropertyCaption() – checks whether the entity attribute was given a localized name.

  • getLocValue() – returns the localized value of the entity attribute based on @LocalizedValue annotation.

  • getMessageRef() – forms a message reference for meta-property which can be used to retrieve the localized name of the entity attribute.

  • getDefaultLocale() – returns default application locale, which is the first one listed in cuba.availableLocales application property.

  • useLocaleLanguageOnly() – returns true, if for all locales supported by the application (defined in cuba.availableLocales property) only the language parameter is specified, without country and variant. This method is used by platform mechanisms which need to find the most appropriate supported locale when locale info is received from the external sources such as operation system or HTTP request.

  • trimLocale() – deletes from the passed locale everything except language, if useLocaleLanguageOnly() method returns true.

You can override MessageTools to extend the set of its methods in your application. Below are the examples of working with the extended interface:

MyMessageTools tools = messages.getTools();
tools.foo();
((MyMessageTools) messages.getTools()).foo();
5.2.6.3. Metadata

Metadata interface provides access to metadata session and view repository.

Interface methods:

  • getSession() – returns the metadata session instance.

  • getViewRepository() – returns the view repository instance.

  • getExtendedEntities() – returns ExtendedEntities instance, intended for working with the extended entities. See more in Extending an Entity.

  • create() – creates an entity instance, taking into account potential extension. See more in Extending an Entity.

  • getTools() – returns MetadataTools interface instance (see below).

5.2.6.3.1. MetadataTools

MetadataTools is a managed bean, containing additional methods for working with metadata. You can access MetadataTools interface by either using Metadata.getTools() method, or as any other bean – by means of injection or through AppBeans class.

`MetadataTools `methods:

  • getAllPersistentMetaClasses() – returns the collection of persistent entities meta-classes.

  • getAllEmbeddableMetaClasses() – returns the collection of embeddable entities meta-classes.

  • getAllEnums() – returns the collection of enumeration classes used as entity attributes types.

  • format() – formats the passed value according to data type of the given meta-property.

  • isSystem() – checks if a meta-property is system, i.e. specified in one of the basic entity interfaces.

  • isPersistent() – checks if a meta-property is persistent, i.e. stored in the database.

  • isTransient() – checks if a meta-property or an arbitrary attribute is non-persistent.

  • isEmbedded() – checks if a meta-property is an embedded object.

  • isAnnotationPresent() – checks if an annotation is present on the class or on one of its ancestors.

  • getNamePatternProperties() – returns collection of meta-properties of attributes included in the instance name, returned by Instance.getInstanceName() method. See @NamePattern.

You can override MetadataTools bean in your application to extend the set of its methods. The examples of working with the extended interface:

MyMetadataTools tools = metadata.getTools();
tools.foo();
((MyMetadataTools) metadata.getTools()).foo();
5.2.6.4. Resources

Resources interface maintains resources loading according to the following rules:

  1. If the provided location is a URL, the resource is downloaded from this URL;

  2. If the provided location begins with classpath: prefix, the resource is downloaded from classpath;

  3. If the location is not a URL and it does not begin with classpath:, then:

    1. The file is searched in the configuration folder of application using the provided location as relative pathname. If the file is found, the resource is downloaded from it;

    2. If the resource is not found at the previous steps, it is downloaded from classpath.

In practice, explicit identification of URL or classpath: prefix is rarely used, so resources are usually downloaded either from the configuration folder or from classpath. The resource in the configuration folder overrides the classpath resource with the same name.

Resources methods:

  • getResourceAsStream() – returns InputStream for the provided resource, or null, if the resource is not found. The stream should be closed after it had been used, for example:

    @Inject
    protected Resources resources;
    ...
    InputStream stream = null;
    try {
        stream = resources.getResourceAsStream(resourceLocation);
        ...
    } finally {
        IOUtils.closeQuietly(stream);
    }

    You can also use "try with resources":

    try (InputStream stream = resources.getResourceAsStream(resourceLocation)) {
        ...
    }
  • getResourceAsString() – returns the indicated resource content as string, or null, if the resource is not found.

5.2.6.5. Scripting

Scripting interface is used to compile and load Java and Groovy classes dynamically (i.e. at runtime) as well as to execute Groovy scripts and expressions.

Scripting methods:

  • evaluateGroovy() – executes the Groovy expression and returns its result.

    cuba.groovyEvaluatorImport application property is used to define the common set of the imported classes inserted into each executed expression. By default, all standard application blocks import PersistenceHelper class.

    The compiled expressions are cached, and this considerably speeds up repeated execution.

    Example:

    @Inject
    protected Scripting scripting;
    ...
    Integer intResult = scripting.evaluateGroovy("2 + 2", new Binding());
    
    Binding binding = new Binding();
    binding.setVariable("instance", new User());
    Boolean boolResult = scripting.evaluateGroovy("return PersistenceHelper.isNew(instance)", binding);
  • runGroovyScript() – executes Groovy script and returns its result.

    The script should be located either in application configuration folder or in classpath (the current Scripting implementation supports classpath resources within JAR files only). A script in the configuration folder overrides the script in classpath with the same name.

    The path to the script is constructed using separators /. The separator is not required in the beginning of the path.

    Example:

    @Inject
    protected Scripting scripting;
    ...
    Binding binding = new Binding();
    binding.setVariable("itemId", itemId);
    BigDecimal amount = scripting.runGroovyScript("com/abc/sales/CalculatePrice.groovy", binding);
  • loadClass() – loads Java or Groovy class using the following steps:

    1. If the class is already loaded, it will be returned.

    2. The Groovy source code (file *.groovy) is searched in the configuration folder. If it is found, it will be compiled and the class will be returned.

    3. The Java source code (file *.java) is searched in the configuration folder. If it is found, it will be compiled and the class will be returned.

    4. The compiled class is searched in classpath. If it is found, it will be loaded and returned.

    5. If nothing is found, null will be returned.

    The files in configuration folder containing Java and Groovy source code can be modified at runtime. On the next loadClass() call the corresponding class will be recompiled and the new one will be returned, with the following restrictions:

    • The type of the source code must not be changed from Groovy to Java;

    • If Groovy source code was once compiled, the deletion of the source code file will not lead to loading of another class from classpath. Instead of this, the class compiled from the removed source code will still be returned.

    Example:

    @Inject
    protected Scripting scripting;
    ...
    Class calculatorClass = scripting.loadClass("com.abc.sales.PriceCalculator");
  • getClassLoader() – returns ClassLoader, which is able to work according to the rules for loadClass() method described above.

Cache of the compiled classes can be cleaned at runtime using CachingFacadeMBean JMX bean.

5.2.6.6. Security

This interface provides authorization – checking user access rights to different objects in the system. Most of the interface methods delegate to the corresponding methods of current UserSession object, but before this they search for an original meta-class of the entity, which is important for projects with extensions. Besides methods duplicating UserSession functionality, this interface contains isEntityAttrReadPermitted() and isEntityAttrUpdatePermitted() methods that check attribute path availability with respect to availability of all attributes and entities included in the path.

The Security interface is recommended to use everywhere instead of direct calling of the UserSession.isXYXPermitted() methods.

See more in User Authentication.

5.2.6.7. TimeSource

TimeSource interface provides the current time. Using new Date() and similar methods in the application code is not recommended.

Examples:

@Inject
protected TimeSource timeSource;
...
Date date = timeSource.currentTimestamp();
long startTime = AppBeans.get(TimeSource.class).currentTimeMillis();
5.2.6.8. UserSessionSource

The interface is used to obtain current user session object. See more in User Authentication.

5.2.6.9. UuidSource

The interface is used to obtain UUID values, including those used for entity identifiers. Using UUID.randomUUID() in the application code is not recommended.

To call from a static context, you can use the UuidProvider class, which also has an additional fromString() method that works faster than the standard UUID.fromString() method.

5.2.6.10. DataManager

DataManager interface provides CRUD functionality on both middle and client tiers. It is a universal tool for loading entity graphs from the database and saving changed detached entity instances.

Tip

See DataManager vs. EntityManager for information on differences between DataManager and EntityManager.

DataManager in fact just delegates to a DataStore implementation and handles cross-database references if needed. The most implementation details described below are in effect when you work with entities stored in a relational database through the standard RdbmsStore. For another type of data store, everything except the interface method signatures can be different. For simplicity, when we write "DataManager" without additional clarification, we mean "DataManager via RdbmsStore".

DataManager methods are listed below:

  • load(), loadList() – loads a graph of entities according to the parameters of the LoadContext object passed to it.

    LoadContext must include either a JPQL query or an entity identifier. If both are defined, the query is used, and the identifier is ignored. The rules for queries creation are similar to those described in Executing JPQL Queries . The difference is that the query in LoadContext may only use named parameters; positional parameters are not supported.

    Examples of loading entities in the screen controller:

    @Inject
    private DataManager dataManager;
    
    private Book loadBookById(UUID bookId) {
        LoadContext<Book> loadContext = LoadContext.create(Book.class)
                .setId(bookId).setView("book.edit");
        return dataManager.load(loadContext);
    }
    
    private List<BookPublication> loadBookPublications(UUID bookId) {
        LoadContext<BookPublication> loadContext = LoadContext.create(BookPublication.class)
                .setQuery(LoadContext.createQuery("select p from library$BookPublication p where p.book.id = :bookId")
                    .setParameter("bookId", bookId))
                .setView("bookPublication.full");
        return dataManager.loadList(loadContext);
    }
  • loadValues() - loads a list of key-value pairs. The method accepts ValueLoadContext which defines a query for scalar values and a list of keys. The returned list contains instances of KeyValueEntity. For example:

    ValueLoadContext context = ValueLoadContext.create()
            .setQuery(ValueLoadContext.createQuery(
                        "select o.customer, sum(o.amount) from demo$Order o " +
                        "where o.date >= :date group by o.customer")
                .setParameter("date", orderDate))
            .addProperty("customer")
            .addProperty("sum");
    List<KeyValueEntity> list = dataManager.loadValues(context);
  • getCount() - returns a number of records for a query passed to the method. When possible, the standard implementation in RdbmsStore executes select count() query with the same conditions as in the original query for maximum performance.

  • commit() – saves a set of entities passed in CommitContext to the database. Collections of entities for updating and deletion must be specified separately.

    The method returns the set of entity instances returned by EntityManager.merge(); essentially these are fresh instances just updated in DB. Further work should be performed with these returned instances to prevent data loss or optimistic locking. You can ensure that required attributes are present in the returned entities by setting a view for each saved instance using CommitContext.getViews() map.

    Examples for saving a collection of entities:

    @Inject
    private DataManager dataManager;
    
    private void saveBookInstances(List<BookInstance> toSave, List<BookInstance> toDelete) {
        CommitContext commitContext = new CommitContext(toSave, toDelete);
        dataManager.commit(commitContext);
    }
    
    private Set<Entity> saveAndReturnBookInstances(List<BookInstance> toSave, View view) {
        CommitContext commitContext = new CommitContext();
        for (BookInstance bookInstance : toSave) {
            commitContext.addInstanceToCommit(bookInstance, view);
        }
        return dataManager.commit(commitContext);
    }
  • reload() - convenience methods to reload a specified instance from the database with the required view. They delegate to load() method.

  • remove() - removes a specified instance from the database. Delegates to commit() method.

Transactions

DataManager always starts a new transaction and commits it on operation completion, thus returning entities in the detached state.

Partial entities

By default, DataManager loads partial entities according to views. It will load all local attributes and use views only for fetching references in the following cases:

  • The loaded entity is cached.

  • In-memory "read" constraints are defined for an entity.

  • The loadPartialEntities attribute of LoadContext is set to false.

5.2.6.10.1. Security in DataManager

The load(), loadList(), loadValues() and getCount() methods check user’s READ permission for entities being loaded. Additionally, loading entities from the database is subject for access group constraints.

The commit() method checks CREATE permissions for new entities, UPDATE for the updated entities and DELETE for the deleted ones.

By default, DataManager checks permissions on entity operations (READ/CREATE/UPDATE/DELETE) when invoked from a client, and ignores them when invoked from a middleware code. Attribute permissions are not enforced by default.

If you want to check entity operation permissions when using DataManager in your middleware code, obtain a wrapper via DataManager.secure() method and call its methods. Alternatively, you can set the cuba.dataManagerChecksSecurityOnMiddleware application property to turn on security check for the whole application.

Attribute permissions will be enforced only if you additionally set the cuba.entityAttributePermissionChecking application property to true.

Note that access group constraints (row-level security) are always applied regardless of the above conditions.

5.2.6.10.2. Queries with distinct

If a screen contains a table with paging, and JPQL that is used to load data can be modified at run time as a result of applying a generic filter or access group constraints, the following can happen when distinct operator is omitted in JPQL queries:

  • If a collection is joined at the database level, the loaded dataset will contain duplicate rows.

  • On client level, the duplicates disappear in the datasource as they are added to a map (java.util.Map).

  • In case of paged table, a page may show fewer lines than requested, while the total number of lines exceeds requested.

Thus, we recommend including distinct in JPQL queries, which ensures the absence of duplicates in the dataset returned from the database. However, certain DB servers (PostgreSQL in particular) have performance problems when executing SQL queries with distinct if the number of returned records is large (more than 10000).

To solve this, the platform contains a mechanism to operate correctly without distinct at SQL level. This mechanism is enabled by cuba.inMemoryDistinct application property. When activated, it does the following:

  • The JPQL query should still include select distinct.

  • DataManager cuts distinct out of the JPQL query before sending it to ORM.

  • After the data page is loaded by DataManager, it deletes the duplicates and runs additional queries to DB in order to retrieve the necessary number of rows which are then returned to the client.

5.2.6.10.3. Sequential Queries

DataManager can select data from the results of previous requests. This capability is used by the generic filter for sequential application of filters.

The mechanism works as follows:

  • If a LoadContext with defined attributes prevQueries and queryKey is provided, DataManager executes the previous query and saves identifiers of retrieved entities in the SYS_QUERY_RESULT table (corresponding to sys$QueryResult entity), separating the sets of records by user sessions and the query session key queryKey.

  • The current query is modified to be combined with the results of the previous one, so that the resulting data complies with the conditions of both queries combined by AND.

  • The process may be further repeated. In this case the gradually reduced set of previous results is deleted from the SYS_QUERY_RESULT table and refilled again.

The SYS_QUERY_RESULT table should be periodically cleaned of all unnecessary query results left by terminated user sessions. This is done by the deleteForInactiveSessions() method of the QueryResultsManagerAPI bean. In an application with enabled cuba.allowQueryFromSelected property this method should be called by scheduled tasks, for example:

<task:scheduled-tasks scheduler="scheduler">
    <task:scheduled ref="cuba_QueryResultsManager" method="deleteForInactiveSessions" fixed-rate="600000"/>
</task:scheduled-tasks>

5.2.7. PersistenceHelper

A helper class for obtaining the information on persistent entities. Unlike the Persistence and PersistenceTools beans, this class is available on all tiers.

The PersistenceHelper bean has the following methods:

  • isLoaded() - determines if an attribute is loaded from the database. The attribute is loaded if it is included into a view, or if it is a local attribute and a view was not provided to the loading mechanism (EntityManager or DataManager). Only immediate attributes of the entity are supported.

  • isNew() – determines if the passed instance is newly created, i.e., in the New state. Also returns true if this instance is actually in Managed state but newly-persisted in the current transaction, or if it is not a persistent entity.

  • isManaged() - determines if the passed instance is Managed, i.e. attached to a persistence context.

  • isDetached() – determines if the passed instance is in the Detached state. Also returns true, if this instance is not a persistent entity.

  • isSoftDeleted() – determines if the passed entity class supports the soft deletion.

  • getEntityName() – returns the name of the entity specified in the @Entity annotation.

5.2.8. AppContext

AppContext is a system class, which stores references to certain common components for each application block in its static fields:

  • ApplicationContext of Spring Framework.

  • Set of application properties loaded from app.properties files.

  • ThreadLocal variable, storing SecurityContext instances.

  • Collection of application lifecycle listeners (AppContext.Listener).

When the application is started, AppContext is initialized using loader classes, specific for each application block:

  • Middleware loader – AppContextLoader

  • Web Client loader – WebAppContextLoader

  • Web Portal loader – PortalAppContextLoader

  • Desktop Client loader – DesktopAppContextLoader

AppContext can be used in the application code for the following tasks:

  • Registering listeners, triggered after full initialization and before termination of the application, for example:

    AppContext.addListener(new AppContext.Listener() {
        @Override
        public void applicationStarted() {
            System.out.println("Application is ready");
        }
    
        @Override
        public void applicationStopped() {
            System.out.println("Application is closing");
        }
    });

    At the moment of applicationStarted() call:

    • All the beans are fully initialized and their @PostConstruct methods are executed.

    • Static AppBeans.get() methods can be used for obtaining beans.

    • The AppContext.isStarted() method returns true.

    • The AppContext.isReady() method returns false.

    • If cuba.automaticDatabaseUpdate application property is enabled, all database update scripts are successfully executed (in the Middleware block).

    At the moment of applicationStopped() call:

    • All the beans are operational and can be obtained via AppBeans.get() methods.

    • AppContext.isStarted() method returns false.

    • The AppContext.isReady() method returns false.

    A real example of using AppContext.Listener can be found in Running Code on Startup.

  • Getting the application property values, stored in app.properties files in case they are not available through configuration interfaces.

  • Passing SecurityContext to new execution threads, see User Authentication.

5.2.9. Application Properties

Application properties represent named values of different types, which determine various aspects of application configuration and functionality. The platform uses application properties extensively, and you can also employ them to configure application-specific features.

Platform application properties can be classified by intended purpose as follows:

  • Configuration parameters – specify sets of configuration files and certain user interface parameters, i.e. determine the application functionality. Values of configuration parameters are usually defined for the application project at development time.

    For example: cuba.springContextConfig.

  • Deployment parameters – describe various URLs to connect application blocks, DBMS type, security settings etc. Values of deployment parameters are usually depend on the environment where the application instance is installed.

  • Runtime parameters – audit settings, email sending parameters etc. Values of these properties can be changed when needed at the application run time even without restart.

Setting Application Properties

Values of application properties can be set in the database, in the property files, or via Java system properties. Besides, a value set in a file overrides the value with the same name from the database. A value set as a Java system property overrides both values from files and from the database.

Some properties do not support setting values in the database for the following reason: their values are needed when the database is not accessible to the application code yet. These are configuration and deployment parameters mentioned above. So you can only define them in property files or via Java system properties. Runtime parameters can always be set in the database (and possibly be overridden by values in files or system properties).

Typically, an application property is used in one or several application blocks. For example, cuba.persistenceConfig is used only in Middleware, cuba.web.appWindowMode is used in Web Client, while cuba.springContextConfig is used in all blocks. It means that if you need to set some value to a property, you should do it in all blocks that use this property. Properties stored in the database are automatically available to all blocks, so you set them just in one place (in the database table) regardless of what blocks use them. Moreover, there is a standard UI screen to manage properties of this type: see Administration > Application Properties. Properties stored in files should be set separately in the respective files of the blocks.

Tip

When you need to set a value to a platform property, find this property in the documentation. If the documentation states that the property is stored in the database, use the Administration > Application Properties screen to set its value. Otherwise, find out what blocks use the property and define it in the app.properties files of these blocks. For example, if the documentation states that the property is used in all blocks, and your application consists of Middleware and Web Client, you should define the property in the app.properties file of the core module and in the web-app.properties file of the web module. Deployment parameters can also be set outside of project files in the configuration directory. See Storing Properties in Files for details.

Properties From Application Components

An application component can expose properties by defining them in its app-component.xml file. Then if an application which uses the component does not define its own value for the property, the value will be obtained from the component. If the application uses multiple components defining the same property, the actual value in the application will be obtained from the component which is the closest ancestor by the hierarchy of dependencies between components. If there are several components on the same level of the hierarchy, the value is unpredictable.

Additive Properties

Sometimes it is needed to get a combined property value from all application components used in the project. This is especially true for configuration parameters that allow platform mechanisms to configure your application based on the parameters provided by components.

Such properties should be made additive by specifying the plus sign in the beginning of their values. This sign indicates that the property value will be assembled from application components at runtime. For example, cuba.persistenceConfig should be an additive property. In your project, it specifies a persistence.xml file defining your project’s data model. But due to the fact that the real property value will include also persistence.xml files of the application components, the whole data model of your application will include also entities defined in the components.

If you omit + for a property, its value will be obtained only from the current project. It can be useful if you don’t want to inherit some configuration from components, for example, when you define a menu structure.

Programmatic Access to Application Properties

You can access application properties in your code using the following mechanisms:

  • Configuration interfaces. If you define application properties as annotated methods of a configuration interface, the application code will have typed access to the properties. Configuration interfaces allow you to define and access properties of all types of storage: database, files and system properties.

  • The getProperty() method of the AppContext class. If you set a property in a file or as a Java system property, you can read its value using this method. This approach has the following drawbacks:

    • Properties stored in the database are not supported.

    • Unlike invoking an interface method, you have to provide the property name as String.

    • Unlike getting a result of a specific type, you can only get the property value as String.

5.2.9.1. Storing Properties in Files

Properties that determine configuration and deployment parameters are specified in special property files named according to the *app.properties pattern. Each application block contains a set of such files which is defined as follows:

  • For web application blocks (Middleware, Web Client, Web Portal) the set of property files is specified in the appPropertiesConfig parameter of web.xml.

  • For the Desktop Client block the standard way to specify the set of property files is to override the getDefaultAppPropertiesConfig() method in an application class inherited from com.haulmont.cuba.desktop.App.

For example, the set of property files of the Middleware block is specified in the web/WEB-INF/web.xml file of the core module and looks as follows:

<context-param>
    <param-name>appPropertiesConfig</param-name>
    <param-value>
        classpath:com/company/sample/app.properties
        /WEB-INF/local.app.properties
        "file:${catalina.base}/conf/app-core/local.app.properties"
    </param-value>
</context-param>

The classpath: prefix means that the corresponding file can be found in the Java classpath, while file: prefix means that it should be loaded from the file system. A path without such prefix means the path inside the web application relative to its root. Java system properties can be used: in this example, catalina.home is the Tomcat installation path.

An order in which files are declared is important because the values, specified in each subsequent file override the values of the properties with the same name, specified in the preceding files.

The last file in the above set is local.app.properties. It can be used to override application properties upon deployment. If the file does not exist, it is silently ignored. You can create this file on the application server and define all properties specific to the environment in it. As a result, the settings will be separated from the application, and you will be able to update the application without fear of losing the specific configuration information. The Using Tomcat in Production section contains an example of using the local.app.properties file.

For Desktop Client, JVM command line arguments serve as an equivalent of local.app.properties. In this block, the properties loader treats all the arguments containing "=" sign as a key/value pair and uses them to replace corresponding application properties specified in app.properties files.

Tip

Use the following rules when create *.properties files:

  • File encoding – UTF-8.

  • The key can contain Latin letters, numbers, periods and underscores.

  • The value is entered after (=) sign.

  • Do not quote values using " or ' brackets.

  • Set file paths either in UNIX (/opt/haulmont/) or Windows (c:\\haulmont\\) format.

  • You can use \n \t \r codes. The \ sign is a reserved code, use \\ to insert it in a value. See more at: http://docs.oracle.com/javase/tutorial/java/data/characters.html.

  • To enter a multi-line value, use \ sign at the end of each line .

5.2.9.2. Storing Properties in the Database

Application properties that represent runtime parameters are stored in the SYS_CONFIG database table.

Such properties have the following distinctive features:

  • As the property value is stored in the database, it is defined in a single location, regardless of what application blocks use it.

  • The value can be changed and saved at runtime in the following ways:

    • Using the Administration > Application Properties screen.

    • Using the ConfigStorageMBean JMX bean.

    • If the configuration interface has a setter method, you can set the property value in the application code.

  • Property value can be overridden for a particular application block in its *app.properties file or via Java system property with the same name.

It is important to mention, that access to properties stored in the database on the client side leads to Middleware requests. This is less efficient than retrieving properties from local *app.properties files. To reduce the number of requests, the client caches properties for the lifetime of configuration interface implementation instance. Thus, if you need to access the properties of a configuration interface from some UI screen for several times, it is recommended to get the reference to this interface upon screen initialization and save it to a screen controller field for further access.

5.2.9.3. Configuration Interfaces

The configuration interfaces mechanism enables working with application properties using Java interface methods, providing the following benefits:

  • Typed access – application code works with actual data types (String, Boolean, Integer etc.).

  • Instead of string property identifiers, the application code uses interface methods, which are checked by the compiler and you can use code completion when working in an IDE.

Example of reading the transaction timeout value in the Middleware block:

@Inject
private ServerConfig serverConfig;

public void doSomething() {
    int timeout = serverConfig.getDefaultQueryTimeoutSec();
    ...
}

If injection is impossible, the configuration interface reference can be obtained via the Configuration infrastructure interface:

int timeout = AppBeans.get(Configuration.class)
        .getConfig(ServerConfig.class)
        .getDefaultQueryTimeoutSec();
Warning

Configuration interfaces are not regular Spring managed beans. They can only be obtained through explicit interface injection or via Configuration.getConfig() but not through AppBeans.get().

5.2.9.3.1. Using Configuration Interfaces

To create a configuration interface in your application, do the following:

  • Create an interface inherited from com.haulmont.cuba.core.config.Config (not to be confused with the entity class com.haulmont.cuba.core.entity.Config).

  • Add @Source annotation to specify where the property values should be stored:

    • SourceType.SYSTEM – values will be taken from the system properties of the given JVM using the System.getProperty() method.

    • SourceType.APP – values will be taken from *app.properties files.

    • SourceType.DATABASE – values will be taken from the database.

  • Create property access methods (getters / setters). If you are not going to change the property value from the application code, do not create setter. A getter return type defines the property type. Possible property types are described below.

  • Add @Property annotation defining the property name to the getter.

  • You can optionally set @Source annotation for a particular property if its source differs from the interface source.

Example:

@Source(type = SourceType.DATABASE)
public interface SalesConfig extends Config {

    @Property("sales.companyName")
    String getCompanyName();
}

Do not create any implementation classes because the platform will create a required proxy automatically when you inject the configuration interface or obtain it through Configuration.

5.2.9.3.2. Property Types

The following property types are supported in the platform out-of-the-box:

  • String, primitive types and their object wrappers (boolean, Boolean, int, Integer, etc.)

  • enum. The property value is stored in a file or in the database as the value name of the enumeration.

    If the enum implements the EnumClass interface and has the static fromId() method for getting a value by an identifier, you can specify that the enum identifier should be stored instead of value with the @EnumStore annotation. For example:

    @Property("myapp.defaultCustomerGrade")
    @DefaultInteger(10)
    @EnumStore(EnumStoreMode.ID)
    CustomerGrade getDefaultCustomerGrade();
    
    @EnumStore(EnumStoreMode.ID)
    void setDefaultCustomerGrade(CustomerGrade grade);
  • Persistent entity classes. When accessing a property of the entity type, the instance defined by the property value is loaded from the database.

To support arbitrary types, use TypeStringify and TypeFactory classes to convert the value to/from a string and specify these classes for the property with @Stringify and @Factory annotations.

Let us consider this process using the UUID type as an example.

  • Create class com.haulmont.cuba.core.config.type.UuidTypeFactory inherited from com.haulmont.cuba.core.config.type.TypeFactory and implement the following method in it:

    public Object build(String string) {
        if (string == null) {
            return null;
        }
        return UUID.fromString(string);
    }
  • There is no need to create TypeStringify as toString() method is sufficient in this case.

  • Annotate the property in the configuration interface:

    @Factory(factory = UuidTypeFactory.class)
    UUID getUuidProp();
    void setUuidProp(UUID value);

The platform provides TypeFactory implementations for the following types:

  • UUIDUuidTypeFactory, as described above.

  • java.util.DateDateFactory. Date value must be specified in yyyy-MM-dd HH:mm:ss.SSS format, for example:

    cuba.test.dateProp = 2013-12-12 00:00:00.000
  • List<Integer> (the list of integers) – IntegerListTypeFactory. The property value must be specified in the form of numbers, separated by spaces, for example:

    cuba.test.integerListProp = 1 2 3
  • List<String> (the list of strings) – StringListTypeFactory. The property value must be specified as a list of strings separated by "|" sign, for example:

    cuba.test.stringListProp = aaa|bbb|ccc
5.2.9.3.3. Default Values

You can specify default values for properties defined by configuration interfaces. These values will be returned instead of null if the property is not set in the storage location – the database or *app.properties files.

A default value can be specified as a string using the @Default annotation, or as a specific type using other annotations from com.haulmont.cuba.core.config.defaults package:

@Property("cuba.email.adminAddress")
@Default("address@company.com")
String getAdminAddress();

@Property("cuba.email.delayCallCount")
@Default("2")
int getDelayCallCount();

@Property("cuba.email.defaultSendingAttemptsCount")
@DefaultInt(10)
int getDefaultSendingAttemptsCount();

@Property("cuba.test.dateProp")
@Default("2013-12-12 00:00:00.000")
@Factory(factory = DateFactory.class)
Date getDateProp();

@Property("cuba.test.integerList")
@Default("1 2 3")
@Factory(factory = IntegerListTypeFactory.class)
List<Integer> getIntegerList();

@Property("cuba.test.stringList")
@Default("aaa|bbb|ccc")
@Factory(factory = StringListTypeFactory.class)
List<String> getStringList();

A default value for an entity is a string of the {entity_name}-{id}-{optional_view_name} format, for example:

@Default("sec$User-98e5e66c-3ac9-11e2-94c1-3860770d7eaf-browse")
User getAdminUser();

@Default("sec$Role-a294aef0-3ac9-11e2-9433-3860770d7eaf")
Role getAdminRole();

5.2.10. Messages Localization

Applications based on CUBA platform support messages localization, which means that all user interface elements can be displayed in the language, selected by user.

Language selection options are determined by the combination of cuba.localeSelectVisible and cuba.availableLocales application properties.

This section describes the localization mechanism and rules of localized messages creation. For information about obtaining messages see Getting Localized Messages.

5.2.10.1. Message Packs

A message pack is a set of property files with the names in messages{_XX}.properties format located in a single Java package. XX suffix indicates the language of the messages in this file and corresponds to the language code in Locale.getLanguage(). It is also possible to use other Locale attributes, for example, country. In this case the message pack file will look like messages{_XX_YY}.properties. One of the files in the pack can have no language suffix – it is the default file. The name of the message pack corresponds to the name of the Java package, which contains the pack files.

Let us consider the following example:

/com/abc/sales/gui/customer/messages.properties
/com/abc/sales/gui/customer/messages_fr.properties
/com/abc/sales/gui/customer/messages_ru.properties

This pack consists of 3 files – one for Russian, one for French and a default file. The name of the pack is com.abc.sales.gui.customer.

Message files contain key/value pairs, where the key is the message identifier referenced by the application code, and the value is the message itself in the language of the file. The rules for matching pairs are similar to those of java.util.Properties property files with the following specifics:

  • File encoding – UTF-8 only.

  • Including other message packs is supported using @include key. Several packs can be included using comma-separated list. In this case, if some message key is found in both the current and the included pack, the message from the current pack will be used. Example of including packs:

    @include=com.haulmont.cuba.web, com.abc.sales.web
    
    someMessage=Some Message
    ...

Messages are retrieved from the packs using Messages interface methods according to the following rules:

  • At first step the search is performed in the application configuration directory.

    • messages_XX.properties file is searched in the directory specified by the message pack name, where XX is the code of the required language.

    • If there is no such file, default messages.properties file is searched in the same directory.

    • If either the required language file or the default file is found, it is loaded together with all @include files, and the key message is searched in it.

    • If the file is not found or it does not contain the proper key, the directory is changed to the parent one and the search procedure is repeated. The search continues until the root of the configuration directory is reached.

  • If the message is not found in the configuration directory, the search is performed in classpath according to the same algorithm.

  • If the message is found, it is cached and returned. If not, the fact that the message is not present is cached as well and the key which was passed for search is returned. Thus, the complex search procedure is only performed once and further on the result is loaded from the local cache of the application block.

Tip

It is recommended to organize message packs as follows:

  • If the application is not intended for internationalization, you can include message strings directly into the application code instead of using packs or use messages.properties default files to separate resources from code.

  • If the application is international, it is reasonable to use default files for the language of the application primary audience or for the English language, so that the messages from these default files are displayed to the user if the messages in the required language are not found.

5.2.10.2. Main Message Pack

Each standard application block should have its own main message pack. For the client tier blocks the main message pack contains main menu entries and common UI elements names (for example, names of OK and Cancel buttons). The main pack also determines Datatype transformation formats for all application blocks, including Middleware.

cuba.mainMessagePack application property is used to specify the main message pack. The property value can be either a single pack or list of packs separated by spaces. For example:

cuba.mainMessagePack=com.haulmont.cuba.web com.abc.sales.web

In this case the messages in the second pack of the list will override those from the first pack. Thus, the messages defined in the application components packs can be overridden in the application project.

Existing messages from CUBA base projects can be also overridden by specifying new messages in the project’s main message pack:

com.haulmont.cuba.gui.backgroundwork/backgroundWorkProgress.timeoutMessage = Overridden Error Message
5.2.10.3. Entity and Attributes Names Localization

To display localized names of the entities and attributes in UI, create special message packs in the Java packages containing the entities. Use the following format in message files:

  • Key of the entity name – simple class name (without package).

  • Key of the attribute name – simple class name, then the name of the attribute separated by period.

The example of default English localization of com.abc.sales.entity.Customer entity – /com/abc/sales/entity/messages.properties file:

Customer=Customer
Customer.name=Name
Customer.email=Email

Order=Order
Order.customer=Customer
Order.date=Date
Order.amount=Amount

Such message packs are usually used implicitly by the framework, for example, by Table and FieldGroup visual components. Besides, you can obtain the names of the entities and attributes using the following methods:

  • Programmatically – by MessageTools getEntityCaption(), getPropertyCaption() methods;

  • In XML screen descriptor – by reference to the message according to MessageTools.loadString() rules: msg://{entity_package}/{key}, for example:

    caption="msg://com.abc.sales.entity/Customer.name"
5.2.10.4. Enum Localization

To localize the enumeration names and values, add messages with the following keys to the message pack located in the Java package of the enumeration class:

  • Enumeration name key – simple class name (without package);

  • Value key – simple class name, then the value name separated by period.

For example, for enum

package com.abc.sales;

public enum CustomerGrade {
    PREMIUM,
    HIGH,
    STANDARD
}

default English localization file /com/abc/sales/messages.properties should contain the following lines:

CustomerGrade=Customer Grade
CustomerGrade.PREMIUM=Premium
CustomerGrade.HIGH=High
CustomerGrade.STANDARD=Standard

Localized enum values are automatically used by different visual components such as LookupField. You can obtain localized enum value programmatically: use getMessage() method of the Messages interface and simply pass the enum instance to it.

5.2.11. User Authentication

This section describes some access control aspects from the developer’s point of view. For complete information on configuring user data access restrictions, see Security Subsystem.

5.2.11.1. UserSession

User session is the main element of access control mechanism of CUBA applications. It is represented by the UserSession object, which is associated with the currently authenticated user and contains information about user rights. The UserSession object can be obtained in any application block using the UserSessionSource infrastructure interface.

The UserSession object is created on Middleware during LoginService.login() method execution after the user is authenticated using a name and a password. The object is then cached in the Middleware block and returned to the client tier. When running in cluster, the session object is replicated to all cluster members. The client tier also stores the session object after receiving it, associating it with the active user in one way or another (for example, storing it in HTTP session). Further on, all Middleware invocations on behalf of this user are accompanied by passing the session identifier (of UUID type). This process does not need any special support in the application code, as the session identifier is passed automatically, regardless of the signature of invoked methods. Processing of client invocations in the Middleware starts from retrieving session from the cache using the obtained identifier. Then the session is associated with the request execution thread. The session object is deleted from the cache when the LoginService.logout() method is called or when the timeout defined by cuba.userSessionExpirationTimeoutSec application property expires.

Thus the session identifier created when the user logs into the system is used for user authentication during each Middleware invocation.

The UserSession object also contains methods for current user authorization – validation of the rights to system objects: isScreenPermitted(), isEntityOpPermitted(), isEntityAttrPermitted(), isSpecificPermitted(). However, it is recommended to use the Security infrastructure interface for programmatic authorization.

The UserSession object can contain named attributes of arbitrary serializable type. The attributes are set by setAttribute() method and returned by getAttribute() method. The latter is also able to return the following session parameters, as if they were attributes:

  • userId – ID of the currently registered or substituted user;

  • userLogin – login of the currently registered or substituted user in lowercase.

The session attributes are replicated in the Middleware cluster, same as the other user session data.

5.2.11.2. Login

Standard user login process:

  • The user enters his username and password.

  • Application client block hashes the password using getPlainHash() method of PasswordEncryption bean and invokes LoginService.login() Middleware method passing the user login and password hash to it.

  • LoginService delegates execution to the LoginWorker bean, which loads User object by the entered login, hashes the obtained password hash again using user identifier as salt and compares the obtained hash to the password hash stored in the DB. In case of mismatch, LoginException is thrown.

  • If the authentication is successful, all the access parameters of the user (roles list, rights, restrictions and session attributes) are loaded to the created UserSession instance.

  • If the user session logging is enabled, the record with user session information is saved to the database.

Password hashing algorithm is implemented by the EncryptionModule type bean and is specified in cuba.passwordEncryptionModule application property. SHA-1 is used by default.

The platform has a mechanism for the protection against password brute force cracking. The protection is enabled by the cuba.bruteForceProtection.enabled application property on Middleware. If the protection is enabled then the combination of user login and IP address is blocked for a time interval in case of multiple unsuccessful login attempts. A maximum number of login attempts for the combination of user login and IP address is defined by the cuba.bruteForceProtection.maxLoginAttemptsNumber application property (default value is 5). Blocking interval in seconds is defined by the cuba.bruteForceProtection.blockIntervalSec application property (default value is 60).

It is possible that the user password (actually, password hash) is not stored in the database, but is verified by external means, for example, by means of integration with LDAP. In this case the authentication is in fact performed by the client block, while the Middleware "trusts" the client by creating the session based on user login only, without the password, using LoginService.loginTrusted() method. This method requires satisfying the following conditions:

Login to the system is also required for scheduled automatic processes as well as for connecting to the Middleware beans using JMX interface. Formally, these actions are considered administrative and they do not require authentication as long as no entities are changed in the database. When an entity is persisted to the database, the process requires login of the user who is making the change so that the login of the user responsible for the changes is stored.

An additional benefit from login to the system for an automatic process or for JMX call is that the server log output is displayed with the current user login if the user session is set to the execution thread. This simplifies searching messages created by specific process during log parsing.

System access for the processes within Middleware is done using LoginWorker.loginSystem() call passing the login (without password) of the user on whose behalf the process will be executed. As result, UserSession object will be created and cached in the corresponding Middleware block but it will not be replicated in the cluster.

See more about processes authentication inside Middleware in System Authentication.

5.2.11.3. SecurityContext

SecurityContext class instance stores information about the user session for the current execution thread. It is created and passed to AppContext.setSecurityContext() method in the following moments:

  • For the Web Client and Web Portal blocks – at the beginning of processing of each HTTP request from the user browser.

  • For the Middleware block – at the beginning of processing of each request from the client tier and from CUBA Scheduled Tasks.

  • For the Desktop Client block – once after the user login, as the desktop application is running in single user mode.

In the first two cases, SecurityContext is removed from the execution thread when the request execution is finished.

If you create a new execution thread from the application code, pass the current SecurityContext instance to it as in the example below:

final SecurityContext securityContext = AppContext.getSecurityContext();
executor.submit(new Runnable() {
    public void run() {
        AppContext.setSecurityContext(securityContext);
        // business logic here
    }
});

The same can be done using SecurityContextAwareRunnable or SecurityContextAwareCallable wrappers, for example:

executor.submit(new SecurityContextAwareRunnable<>(() -> {
     // business logic here
}));
Future<String> future = executor.submit(new SecurityContextAwareCallable<>(() -> {
    // business logic here
    return some_string;
}));

5.2.12. Exceptions Handling

This section describes various aspects of working with exceptions in CUBA applications.

5.2.12.1. Exception Classes

The following rules should be followed when creating your own exception classes:

  • If the exception is part of business logic and requires some non-trivial actions to handle it, the exception class should be made checked (inherited from Exception). Such exceptions are handled by the invoking code.

  • If the exception indicates an error and assumes interruption of execution and a simple action like displaying the error information to the user, its class should be unchecked (inherited from RuntimeException). Such exceptions are processed by special handler classes registered in the client blocks of the application.

  • If the exception is thrown and processed in the same block, its class should be declared in corresponding module. If the exception is thrown on Middleware and processed on the client tier, the exception class should be declared in the global module.

The platform contains a special unchecked exception class SilentException. It can be used to interrupt execution without showing any messages to the user or writing them to the log. SilentException is declared in the global module, and therefore is accessible both in Middleware and client blocks.

5.2.12.2. Passing Middleware Exceptions

If an exception is thrown on Middleware as a result of handling a client request, the execution terminates and the exception object is returned to the client. The object usually includes the chain of underlying exceptions. This chain can contain classes which are inaccessible for the client tier (for example, JDBC driver exceptions). For this reason, instead of sending this chain to the client we send its representation inside a specially created RemoteException object.

The information about the causing exceptions is stored as a list of RemoteException.Cause objects. Each Cause object always contains an exception class name and its message. Moreover, if the exception class is "supported by client", Cause stores the exception object as well. This enables passing information to the client in the exception fields.

Exception class should be annotated by @SupportedByClient if its objects should be passed to the client tier as Java objects. For example:

@SupportedByClient
public class WorkflowException extends RuntimeException {
...

Thus, when an exception is thrown on Middleware and it is not annotated by @SupportedByClient the calling client code will receive RemoteException containing original exception information in a string form. If the source exception is annotated by @SupportedByClient, the caller will receive it directly. This enables handling the exceptions declared by Middleware services in the application code in the traditional way – using try/catch blocks.

Bear in mind that if you need the exception supported by client to be passed on the client as an object, it should not contain any unsupported exceptions in its getCause() chain. Therefore, if you create an exception instance on Middleware and want to pass it to the client, specify cause parameter only if you are sure that it contains the exceptions known to the client.

ServiceInterceptor class is a service interceptor which packs the exception objects before passing them to the client tier. Besides, it performs exceptions logging. All information about the exception including full stack trace is logged by default. If it is not desirable, add @Logging annotation to the exception class and specify the logging level:

  • FULL – full information, including stacktrace (default).

  • BRIEF – exception class name and message only.

  • NONE – no output.

For example:

@SupportedByClient
@Logging(Logging.Type.BRIEF)
public class FinancialTransactionException extends Exception {
...
5.2.12.3. Client-Level Exception Handlers

Unhandled exceptions in Web Client and Desktop Client blocks thrown on the client tier or passed from Middleware, are passed to the special handlers mechanism. This mechanism is implemented in the gui module and available for both blocks.

The handler should be a managed bean implementing the GenericExceptionHandler interface, handle processing in its handle() method and return true, or immediately return false, if this handler is not able to handle the passed exception. This behaviour enables creating a "chain of responsibility" for handlers.

It is recommended to inherit your handlers from the AbstractGenericExceptionHandler base class, which is able to disassemble the exceptions chain (including ones packed inside RemoteException) and handle specific exception types. Exception types supported by this handler are defined by passing strings array to the base constructor from the handler constructor. Each string of the array should contain one full class name of the handled exception, for example:

@Component("cuba_EntityAccessExceptionHandler")
public class EntityAccessExceptionHandler extends AbstractGenericExceptionHandler {

    public EntityAccessExceptionHandler() {
        super(EntityAccessException.class.getName());
    }
...

If the exception class is not accessible on the client side, specify its name with the string literal:

@Component("cuba_OptimisticExceptionHandler")
public class OptimisticExceptionHandler extends AbstractGenericExceptionHandler implements Ordered {

    public OptimisticExceptionHandler() {
        super("javax.persistence.OptimisticLockException");
    }
...

In the case of using AbstractGenericExceptionHandler as a base class, the processing logic is located in doHandle() method and looks as follows:

@Override
protected void doHandle(String className, String message, @Nullable Throwable throwable, WindowManager windowManager) {
    String msg = messages.getMainMessage("zeroBalance.message");
    windowManager.showNotification(msg, IFrame.NotificationType.ERROR);
}

If the name of the exception class is insufficient to make a decision whether this handler can be applied to the exception, canHandle() method should be defined. This method accepts also the text of the exception. If the handler is applicable for this exception, the method must return true. For example:

@Component("cuba_NumericOverflowExceptionHandler")
public class NumericOverflowExceptionHandler extends AbstractGenericExceptionHandler {

    public NumericOverflowExceptionHandler() {
        super(EclipseLinkException.class.getName());
    }

    @Override
    protected boolean canHandle(String className, String message, @Nullable Throwable throwable) {
        return StringUtils.containsIgnoreCase(message, "Numeric field overflow");
    }
...

WindowManager provides a method for showing an exception with its stack trace: showExceptionDialog(). The method has the following parameters:

  • throwable - Throwable instance.

  • caption - dialog caption. It is an optional parameter, if not set, the default caption is used.

  • message - dialog message. It is an optional parameter, if not set, the default caption is used.

An example of using the method in an exception handler:

@Override
protected void doHandle(String className, String message, @Nullable Throwable throwable, WindowManager windowManager) {
    if (throwable != null)
        windowManager.showExceptionDialog(throwable, "Exception is thrown", message);
    else
        windowManager.showNotification(message, IFrame.NotificationType.ERROR);
}

5.2.13. Bean Validation

Bean validation is an optional mechanism that provides uniform validation of data on the middleware, in Generic UI and REST API. It is based on the JSR 349 - Bean Validation 1.1 and its reference implementation: Hibernate Validator.

5.2.13.1. Defining Constraints

You can define constraints using annotations of the javax.validation.constraints package or custom annotations. The annotations can be set on an entity or POJO class declaration, field or getter, and on a middleware service method.

Example of using standard validation annotations on entity fields:

@Table(name = "DEMO_CUSTOMER")
@Entity(name = "demo$Customer")
public class Customer extends StandardEntity {

    @Size(min = 3) // length of value must be longer then 3 characters
    @Column(name = "NAME", nullable = false)
    protected String name;

    @Min(1) // minimum value
    @Max(5) // maximum value
    @Column(name = "GRADE", nullable = false)
    protected Integer grade;

    @Pattern(regexp = "\\S+@\\S+") // value must conform to the pattern
    @Column(name = "EMAIL")
    protected String email;

    //...
}

Example of using a custom class-level annotation (see below):

@CheckTaskFeasibility(groups = {Default.class, UiCrossFieldChecks.class}) // custom validation annotation
@Table(name = "DEMO_TASK")
@Entity(name = "demo$Task")
public class Task extends StandardEntity {
    //...
}

Example of validation of a service method parameters and return value:

public interface TaskService {
    String NAME = "demo_TaskService";

    @Validated // indicates that the method should be validated
    @NotNull
    String completeTask(@Size(min = 5) String comment, @NotNull Task task);
}
Constraint Groups

Constraint groups enable applying only a subset of all defined constraints depending on the application logic. For example, you may want to force a user to enter a value for an entity attribute, but at the same time to have an ability to set this attribute to null by some internal mechanism. In order to do it, you should specify the groups attribute on the constraint annotation. Then the constraint will take effect only when the same group is passed to the validation mechanism.

The platform passes to the validation mechanism the following constraint groups:

  • RestApiChecks - when validating in REST API.

  • ServiceParametersChecks - when validating service parameters.

  • ServiceResultChecks - when validating service return values.

  • UiComponentChecks - when validating individual UI fields.

  • UiCrossFieldChecks - when validating class-level constraints on entity editor commit.

  • javax.validation.groups.Default - this group is always passed except on the UI editor commit.

Validation Messages

Constraints can have messages to be displayed to users.

Messages can be set directly in the validation annotations, for example:

@Pattern(regexp = "\\S+@\\S+", message = "Invalid format")
@Column(name = "EMAIL")
protected String email;

You can also place the message in a localized messages pack and use the following format to specify the message in an annotation: {msg://message_pack/message_key}. For example:

@Pattern(regexp = "\\S+@\\S+", message = "{msg://com.company.demo.entity/Customer.email.validationMsg}")
@Column(name = "EMAIL")
protected String email;

Messages can contain parameters and expressions. Parameters are enclosed in {} and represent either localized messages or annotation parameters, e.g. {min}, {max}, {value}. Expressions are enclosed in ${} and can include the validated value variable validatedValue, annotation parameters like value or min, and JSR-341 (EL 3.0) expressions. For example:

@Pattern(regexp = "\\S+@\\S+", message = "Invalid email: ${validatedValue}, pattern: {regexp}")
@Column(name = "EMAIL")
protected String email;

Localized message values can also contain parameters and expressions.

Custom Constraints

You can create you own domain-specific constraints with programmatic or declarative validation.

In order to create a constraint with programmatic validation, do the following:

  1. Create an annotation in the global module of your project and annotate it with @Constraint. The annotation must contain message, groups and payload attributes:

    @Target({ ElementType.TYPE })
    @Retention(RUNTIME)
    @Constraint(validatedBy = TaskFeasibilityValidator.class)
    public @interface CheckTaskFeasibility {
    
        String message() default "{msg://com.company.demo.entity/CheckTaskFeasibility.message}";
    
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default {};
    }
  2. Create a validator class in the global module of your project:

    public class TaskFeasibilityValidator implements ConstraintValidator<CheckTaskFeasibility, Task> {
    
        @Override
        public void initialize(CheckTaskFeasibility constraintAnnotation) {
        }
    
        @Override
        public boolean isValid(Task value, ConstraintValidatorContext context) {
            Date now = AppBeans.get(TimeSource.class).currentTimestamp();
            return !(value.getDueDate().before(DateUtils.addDays(now, 3)) && value.getProgress() < 90);
        }
    }
  3. Use the annotation:

    @CheckTaskFeasibility(groups = UiCrossFieldChecks.class)
    @Table(name = "DEMO_TASK")
    @Entity(name = "demo$Task")
    public class Task extends StandardEntity {
    
        @Future
        @Temporal(TemporalType.DATE)
        @Column(name = "DUE_DATE")
        protected Date dueDate;
    
        @Min(0)
        @Max(100)
        @Column(name = "PROGRESS", nullable = false)
        protected Integer progress;
    
        //...
    }

You can also create custom constraints using a composition of existing ones, for example:

@NotNull
@Size(min = 2, max = 14)
@Pattern(regexp = "\\d+")
@Target({METHOD, FIELD, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = {})
public @interface ValidProductCode {
    String message() default "{msg://om.company.demo.entity/ValidProductCode.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

When using a composite constraint, the resulting set of constraint violations will contain separate entries for each enclosed constraint. If you want to return a single violation, annotate the annotation class with @ReportAsSingleViolation.

5.2.13.2. Running Validation
Validation in UI

Generic UI components connected to a datasource get an instance of BeanValidator to check the field value. The validator is invoked from the Component.Validatable.validate() method implemented by the visual component and can throw the CompositeValidationException exception that contains the set of violations.

The standard validator can be removed or initialized with a different constraint group:

public class Screen extends AbstractWindow {

    @Inject
    private TextField field1;
    @Inject
    private TextField field2;

    public void init(Map<String, Object> params) {
        // Completely remove bean validation from the UI component
        field1.getValidators().stream()
                .filter(validator -> validator instanceof BeanValidator)
                .forEach(textField::removeValidator);

        // Here validators will check only constraints with explicitly set UiComponentChecks group, because
        // the Default group will not be passed
        field2.getValidators().stream()
                .filter(validator -> validator instanceof BeanValidator)
                .forEach(validator -> {
                    ((BeanValidator) validator).setValidationGroups(new Class[] {UiComponentChecks.class});
                });
    }
}

By default, BeanValidator has both Default and UiComponentChecks groups.

If an entity attribute is annotated with @NotNull without constraint groups, it will be marked as mandatory in metadata and UI components working with this attribute through a datasource will have required = true.

The DateField and DatePicker components automatically set their rangeStart and rangeEnd properties by the @Past and @Future annotations, but without considering time.

Editor screens perform validation against class-level constraints on commit if the constraint includes the UiCrossFieldChecks group and if all attribute-level checks are passed. You can turn off the validation of this kind using the crossFieldValidate property of the screen in the screen XML descriptor or in the controller:

<window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
        caption="msg://editorCaption"
        class="com.company.demo.web.task.TaskEdit"
        datasource="taskDs"
        crossFieldValidate="false">
    <!-- ... -->
</window>
public class TaskEdit extends AbstractEditor<Task> {
    @Override
    public void init(Map<String, Object> params) {
        setCrossFieldValidate(false);
    }
}
Validation in Middleware Services

Middleware services perform validation of parameters and results if a method has annotation @Validated in the service interface. For example:

public interface TaskService {
    String NAME = "demo_TaskService";

    @Validated
    @NotNull
    String completeTask(@Size(min = 5) String comment, @NotNull Task task);
}

The @Validated annotation can specify constraint groups to apply a certain set of constraints. If no groups are specified, the following are used by default:

  • Default and ServiceParametersChecks - for method parameters

  • Default and ServiceResultChecks - for method return value

The MethodParametersValidationException and MethodResultValidationException exceptions are thrown on validation errors.

If you perform some custom programmatic validation in a service, use CustomValidationException to inform clients about validation errors in the same format as the standard bean validation does. It can be particularly relevant for REST API clients.

Validation in REST API

Universal REST API automatically performs bean validation for create and update actions. Validation errors are returned to the client in the following way:

  • MethodResultValidationException and ValidationException cause 500 Server error HTTP status

  • MethodParametersValidationException, ConstraintViolationException and CustomValidationException cause 400 Bad request HTTP status

  • Response body with Content-Type: application/json will contain a list of objects with message, messageTemplate, path and invalidValue properties, for example:

    [
        {
            "message": "Invalid email: aaa",
            "messageTemplate": "{msg://com.company.demo.entity/Customer.email.validationMsg}",
            "path": "email",
            "invalidValue": "aaa"
        }
    ]
    • path indicates a path to the invalid attribute in the validated object graph

    • messageTemplate contains a string which is defined in the message annotation attribute

    • message contains an actual value of the validation message

    • invalidValue is returned only if its type is one of the following: String, Date, Number, Enum, UUID.

Programmatic Validation

You can perform bean validation programmatically using the BeanValidation infrastructure interface, available on both middleware and client tier. It is used to obtain a javax.validation.Validator implementation which runs validation. The result of validation is a set of ConstraintViolation objects. For example:

@Inject
private BeanValidation beanValidation;

public void save(Foo foo) {
    Validator validator = beanValidation.getValidator();
    Set<ConstraintViolation<Foo>> violations = validator.validate(foo);
    // ...
}

5.3. Database Components

This section provides information on how to configure the application for working with particular DBMS. It also describes a script-based mechanism, which enables creating a new database and keeping it up-to-date throughout the entire cycle of the development and operation of the application.

Database components belong to the Middleware block; other blocks of the application do not have direct access to the database.

Some additional information on working with the database is provided in the Working with Databases section.

5.3.1. DBMS Types

The type of the DBMS used in the application is defined by the cuba.dbmsType and (optionally) cuba.dbmsVersion application properties. These properties affect various platform mechanisms depending on the database type.

The application connects to the database through the javax.sql.DataSource which is extracted from JNDI by the name specified in the cuba.dataSourceJndiName application property (java:comp/env/jdbc/CubaDS by default). Configuration of the data source for standard deployment is defined in the context.xml file of the core module. The data source should use a proper JDBC driver for the selected DBMS.

The platform supports the following types of DBMS "out of the box":

cuba.dbmsType cuba.dbmsVersion

HSQLDB

hsql

PostgreSQL 8.4+

postgres

Microsoft SQL Server 2005, 2008

mssql

Microsoft SQL Server 2012+

mssql

2012

Oracle Database 11g+

oracle

MySQL 5.6+

mysql

The table below describes the recommended mapping of data types between entity attributes in Java and table columns in different DBMS. CUBA Studio automatically chooses these types when generates scripts to create and update the database. The operation of all platform mechanisms is guaranteed when you use these types.

Java HSQL PostgreSQL MS SQL Server Oracle MySQL

UUID

varchar(36)

uuid

uniqueidentifier

varchar2(32)

varchar(32)

Date

timestamp

timestamp

datetime

timestamp

datetime(3)

java.sql.Date

timestamp

date

datetime

date

date

java.sql.Time

timestamp

time

datetime

timestamp

time(3)

BigDecimal

decimal(p, s)

decimal(p, s)

decimal(p, s)

number(p, s)

decimal(p, s)

Double

double precision

double precision

double precision

float

double precision

Long

bigint

bigint

bigint

number(19)

bigint

Integer

integer

integer

integer

integer

integer

Boolean

boolean

boolean

tinyint

char(1)

boolean

String (limited)

varchar(n)

varchar(n)

varchar(n)

varchar2(n)

varchar(n)

String (unlimited)

longvarchar

text

varchar(max)

clob

longtext

byte[]

longvarbinary

bytea

image

blob

longblob

Usually, the whole work to convert the data between the database and the Java code is performed by the ORM layer in conjunction with the appropriate JDBC driver. This means that no manual conversion is required when working with the data using the EntityManager methods and JPQL queries; you should simply use Java types listed in the left column of the table.

When using native SQL through EntityManager.createNativeQuery() or through QueryRunner, some types in the Java code will be different from those mentioned above, depending on DBMS used. In particular, this applies to attributes of the UUID - type – only the PostgreSQL driver returns values of corresponding columns using this type; other servers return String. To abstract application code from the database type, it is recommended to convert parameter types and query results using the DbTypeConverter interface.

5.3.1.1. Support for Other DBMSs

In the application project, you can use any DBMS supported by the ORM framework (EclipseLink). Follow the steps below:

  • Specify the type of database in the form of an arbitrary code in the cuba.dbmsType property. The code must be different from those used in the platform: hsql, postgres, mssql, oracle.

  • Implement the DbmsFeatures, SequenceSupport, DbTypeConverter interfaces by classes with the following names: TypeDbmsFeatures, TypeSequenceSupport, and TypeDbTypeConverter, respectively, where Type is the DBMS type code. The package of the implementation class must be the same as of the interface.

  • Create database init and update scripts in the directories marked with the DBMS type code. Init scripts must create all database objects required by the platform entities (you can copy them from the existing 10-cuba, etc. directories and modify for your database).

  • To create and update the database by Gradle tasks, you need to specify the additional parameters for these tasks in build.gradle:

    task createDb(dependsOn: assemble, type: CubaDbCreation) {
        dbms = 'my'                                            // DBMS code
        driver = 'net.my.jdbc.Driver'                          // JDBC driver class
        dbUrl = 'jdbc:my:myserver://192.168.47.45/mydb'        // Database URL
        masterUrl = 'jdbc:my:myserver://192.168.47.45/master'  // URL of a master DB to connect to for creating the application DB
        dropDbSql = 'drop database mydb;'                      // Drop database statement
        createDbSql = 'create database mydb;'                  // Create database statement
        timeStampType = 'datetime'                             // Date and time datatype - needed for SYS_DB_CHANGELOG table creation
        dbUser = 'sa'
        dbPassword = 'saPass1'
    }
    
    task updateDb(dependsOn: assemble, type: CubaDbUpdate) {
        dbms = 'my'                                            // DBMS code
        driver = 'net.my.jdbc.Driver'                          // JDBC driver class
        dbUrl = 'jdbc:my:myserver://192.168.47.45/mydb'        // Database URL
        dbUser = 'sa'
        dbPassword = 'saPass1'
    }
5.3.1.2. DBMS Version

In addition to cuba.dbmsType application property, there is an optional cuba.dbmsVersion property. It affects the choice of interface implementations for DbmsFeatures, SequenceSupport, DbTypeConverter, and the search for database init and update scripts.

The name of the implementation class of the integration interface is constructed as follows: TypeVersionName. Here, Type is the value of the cuba.dbmsType property (capitalized), Version is the value of cuba.dbmsVersion, and Name is the interface name. The package of the class must correspond to that of the interface. If a class with the same name is not available, an attempt is made to find a class with the name without version: TypeName. If such class does not exist either, an exception is thrown.

For example, the com.haulmont.cuba.core.sys.persistence.Mssql2012SequenceSupport class is defined in the platform. This class will take effect if the following properties are specified in the project:

cuba.dbmsType = mssql
cuba.dbmsVersion = 2012

The search for database init and update scripts prioritizes the type-version directory over the type directory. This means that the scripts in the type-version directory replace the scripts with the same name in the type directory. The type-version directory can also contain some scripts with unique names; they will be added to the common set of scripts for execution, too. Script sorting is performed by path, starting with the first subdirectory of the type or type-version directory, i.e. regardless of the directory where the script is located (versioned or not).

For example, the init script for Microsoft SQL Server versions below and above 2012 should look as follows:

modules/core/db/init/
   mssql/
       10.create-db.sql
       20.create-db.sql
       30.create-db.sql
   mssql-2012/
       10.create-db.sql

5.3.2. Scripts to Create and Update the Database

A CUBA-application project always contains two sets of scripts:

  • Scripts to create the database, intended for the creation of the database from scratch. They contain a set of the DDL and DML operators, which create an empty database schema that is fully consistent with the current state of the data model of the application. These scripts can also fill the database with the necessary initialization data.

  • Scripts to update the database, intended for bringing the database structure to the current state of the data model from any of the previous states.

When changing the data model, it is necessary to reproduce the corresponding change of the database schema in create and update scripts. For example, when adding the address attribute to the Customer entity, it is necessary to:

  1. Change the table creation script:

    create table SALES_CUSTOMER (
      ID varchar(36) not null,
      CREATE_TS timestamp,
      CREATED_BY varchar(50),
      NAME varchar(100),
      ADDRESS varchar(200), -- added column
      primary key (ID)
    )
  2. Add an update script, which modifies the same table:

    alter table SALES_CUSTOMER add ADDRESS varchar(200)

The create scripts are located in the /db/init directory of the core module. For each type of DBMS supported by the application, a separate set of scripts is created and located in the subdirectory specified in cuba.dbmsType application property, for example /db/init/postgres. Create scripts names should have the following format {optional_prefix}create-db.sql.

The update scripts are located in the /db/update directory of the core module. For each type of DBMS supported by the application, a separate set of scripts is created and located in the subdirectory specified in cuba.dbmsType application property, for example, /db/update/postgres.

The update scripts can be of two types: with the *.sql or *.groovy extension. The primary way to update the database is with SQL scripts. Groovy scripts are only executed by the server mechanism to launch database scripts, therefore they are mainly used at the production stage, in cases when migration or import of the data that cannot be implemented in pure SQL.

The update scripts should have names, which form the correct sequence of their execution when sorted in the alphabetical order (usually, it is a chronological sequence of their creation). Therefore, when creating such scripts manually, it is recommended to specify the name of the update scripts in the following format: {yymmdd}-{description}.sql, where yy is a year, mm is a month, dd is a day, and description is a short description of the script. For example, 121003-addCodeToCategoryAttribute.sql. Studio also adheres to this format when generating scripts automatically.

In order to be executed by the updateDb task, the groovy scripts should have .upgrade.groovy extension and the same naming logic. Post update actions are not allowed in that scripts, while the same ds (access to datasource) and log (access to logging) variables are used for the data binding. The execution of groovy scripts can be disabled by setting executeGroovy = false in the updateDb task of build.gradle.

It is possible to group update scripts into subdirectories, however the path to the script with the subdirectory should not break the chronological sequence. For example, subdirectories can be created by using year, or by year and month.

In a deployed application, the scripts to create and update the database are located in a special database script directory, that is set by the cuba.dbDir application property.

5.3.2.1. The Structure of SQL Scripts

Create and update SQL scripts are text files with a set of DDL and DML commands separated by the "^" character. The "^" character is used, so that the ";" separator can be applied as part of complex commands; for example, when creating functions or triggers. The script execution mechanism splits the input file into separate commands using the "^" separator and executes each command in a separate transaction. This means that, if necessary, it is possible to group several single statements (e.g., insert), separated by semicolons and ensure that they execute in a single transaction.

Tip

The "^" delimiter can be escaped by doubling it. For example, if you want to pass ^[0-9\s]+$ to a statement, the script should contain ^^[0-9\s]+$.

An example of the update SQL script:

create table LIBRARY_COUNTRY (
  ID varchar(36) not null,
  CREATE_TS time,
  CREATED_BY varchar(50),
  NAME varchar(100) not null,
  primary key (ID)
)^

alter table LIBRARY_TOWN add column COUNTRY_ID varchar(36) ^
alter table LIBRARY_TOWN add constraint FK_LIBRARY_TOWN_COUNTRY_ID foreign key (COUNTRY_ID) references LIBRARY_COUNTRY(ID)^
create index IDX_LIBRARY_TOWN_COUNTRY on LIBRARY_TOWN (COUNTRY_ID)^
5.3.2.2. The Structure of Groovy scripts

Groovy update scripts have the following structure:

  • The main part, which contains the code executed before the start of the application context. In this section, you can use any Java, Groovy and the Middleware application block classes. However, it should be kept in mind that no beans, infrastructure interfaces and other application objects have yet been instantiated and it is impossible to use them.

    The main part is primarily designed to update the database schema, as usually done with ordinary SQL scripts.

  • The PostUpdate part – a set of closures, which will be executed after the start of the application context and once the update process is finished. Inside these closures, it is possible to use any Middleware objects.

    In this part of the script, it is convenient to perform data import as it is possible to use the Persistence interface and data model objects.

The execution mechanism passes the following variables to the Groovy scripts:

  • ds – instance of javax.sql.DataSource for the application database;

  • log – instance of org.apache.commons.logging.Log to output messages in the server log;

  • postUpdate – object that contains the add(Closure closure) method to add PostUpdate closures described above.

Warning

Groovy scripts are executed only by the server mechanism to launch database scripts.

An example of the Groovy update script:

import com.haulmont.cuba.core.Persistence
import com.haulmont.cuba.core.global.AppBeans
import com.haulmont.refapp.core.entity.Colour
import groovy.sql.Sql

log.info('Executing actions in update phase')

Sql sql = new Sql(ds)
sql.execute """ alter table MY_COLOR add DESCRIPTION varchar(100); """

// Add post update action
postUpdate.add({
    log.info('Executing post update action using fully functioning server')

    def p = AppBeans.get(Persistence.class)
    def tr = p.createTransaction()
    try {
        def em = p.getEntityManager()

        Colour c = new Colour()
        c.name = 'yellow'
        c.description = 'a description'

        em.persist(c)
        tr.commit()
    } finally {
        tr.end()
    }
})

5.3.3. The Execution of Database Scripts by Gradle Tasks

This mechanism is generally used by application developers for updating their own database instance. The execution of scripts essentially comes down to running a special Gradle task from build.gradle build script. This can be done from the command line or via the Studio interface.

To run scripts to create the database, the createDb task is used. In Studio, it corresponds to the RunCreate database command in main menu. When this task is started, the following occurs:

  1. Scripts of the application components and db/**/*.sql scripts of the core module of the current project are built in the modules/core/build/db directory. Sets of scripts for application components are located in subdirectories with numeric prefixes. The prefixes are used to provide the alphabetical order of the execution of scripts according to the dependencies between components.

  2. If the database exists, it is completely erased. A new empty database is created.

  3. All creation scripts from modules/core/build/db/init/**/*create-db.sql subdirectory are executed sequentially in the alphabetical order, and their names along with the path relative to the db directory are registered in the SYS_DB_CHANGELOG table.

  4. Similarly, in the SYS_DB_CHANGELOG table, all currently available modules/core/build/db/update/**/*.sql update scripts are registered. This is required for applying the future incremental updates to the database.

To run scripts to update the database, the updateDb task is used. In Studio, it corresponds to the RunUpdate database command in main menu. When this task is started, the following occurs:

  1. The scripts are built in the same way as for the createDb command described above.

  2. The execution mechanism checks, whether all creation scripts of application components have been run (by checking the SYS_DB_CHANGELOG table). If not, the application component creation scripts are executed and registered in the SYS_DB_CHANGELOG table.

  3. A search is performed in modules/core/build/db/update/** directories, for update scripts which are not registered in the SYS_DB_CHANGELOG table, i.e., not previously executed.

  4. All scripts found in the previous step are executed sequentially in the alphabetical order, and their names along with the path relative to the db directory are registered in the SYS_DB_CHANGELOG table.

5.3.4. The Execution of Database Scripts by the Server

The mechanism to execute database scripts by the server is used for bringing the DB up to date at the start of the application server and is activated during the initialization of the Middleware block. Obviously, the application should have been built and deployed on the server – production or developer’s Tomcat instance.

Depending on the conditions described below, this mechanism either executes create or update scripts, i.e., it can initialize the DB from scratch and update it. However, unlike the Gradle createDb task described in the previous section, the database must exist to be initialized – the server does not create the DB automatically but only executes scripts on it.

The mechanism to execute scripts by the server works as follows:

  • The scripts are extracted from the database scripts directory, defined by the cuba.dbDir application property, which by default is set to tomcat/webapps/app-core/WEB-INF/db.

  • If the DB does not have the SEC_USER table, the database is considered empty and the full initialization is run using the create scripts. After executing the initialization scripts, their names are stored in the SYS_DB_CHANGELOG table. The names of all available update scripts are stored in the same table, without their execution.

  • If the DB has the SEC_USER table but does not have the SYS_DB_CHANGELOG table (this is the case when the described mechanism is launched for the first time on the existing production DB), no scripts are executed. Instead, the SYS_DB_CHANGELOG table is created and the names of all currently available create and update scripts are stored.

  • If the DB has both the SEC_USER and SYS_DB_CHANGELOG tables, the update scripts whose names were not previously stored in the SYS_DB_CHANGELOG table are executed and their names are stored in the SYS_DB_CHANGELOG table. The sequence of scripts execution is determined by two factors: the priority of the application component (see database scripts directory: 10-cuba, 20-bpm, …​) and the name of the script file (taking into account the subdirectories of the update directory) in the alphabetical order.

    Before the execution of update scripts, the check is performed, whether all creation scripts of application components have been run (by checking the SYS_DB_CHANGELOG table). If the database is not initialized for use of some application component, its creation scripts are executed.

The mechanism to execute the scripts on server startup is enabled by the cuba.automaticDatabaseUpdate application property.

In already running application, the script execution mechanism can be launched using the app-core.cuba:type=PersistenceManager JMX bean by calling its updateDatabase() method with the update parameter. Obviously it is only possible to update already existing DB as it is impossible to log in to the system to run a method of the JMX bean with an empty DB. Please note, that an unrecoverable error will occur, if part of the data model no longer corresponding to the outdated DB schema is initialized during Middleware startup or user login. That is why the automatic update of the DB on the server startup before initializing the data model is only universal.

The JMX app-core.cuba:type=PersistenceManager bean has one more method related to the DB update mechanism: findUpdateDatabaseScripts(). It returns a list of new update scripts available in the directory and not registered in the DB (not yet executed).

Recommendations for usage of the server DB update mechanism can be found in Creating and Updating the Database in Production.

5.4. Middleware Components

The following figure shows the main components of the CUBA application middle tier.

Middleware
Figure 12. Components of the Middle Tier

Services are container-managed components that form the application boundary and provide the interface to the client tier. Services may contain the business logic themselves or delegate the execution to managed beans.

Managed beans are container-managed components that contain the business logic of the application. They are called by services, other beans or via the optional JMX interface.

Persistence is the infrastructure interface to access the data storage functionality: ORM and transactions management.

5.4.1. Services

Services form the layer that defines a set of Middleware operations available to the client tier. In other words, a service is an entry point to the Middleware business logic. In a service, you can manage transactions, check user permissions, work with the database or delegate execution to other managed beans of the middle tier.

Below is a class diagram which shows the components of a service:

MiddlewareServices

The service interface is located in the global module and is available for both middle and client tiers. At runtime, a proxy is created for the service interface on the client tier. The proxy provides invocation of service bean methods using Spring HTTP Invoker mechanism.

The service implementation bean is located in the core module and is available on the middle tier only.

ServiceInterceptor is called automatically for any service method using Spring AOP. It checks the availability of the user session for the current thread, and transforms and logs exceptions if the service is called from the client tier.

5.4.1.1. Creating a Service

The name of service interface should end with Service, the names of implementation class – with ServiceBean.

The following steps are required for creating a service:

  1. Create the service interface in the global module, as the service interface must be available at all tiers), and specify the service name in it. It is recommended to specify the name in the following format: {project_name}_{interface_name}. For example:

    package com.sample.sales.core;
    
    import com.sample.sales.entity.Order;
    
    public interface OrderService {
        String NAME = "sales_OrderService";
    
        void calculateTotals(Order order);
    }
  2. Create the service class in the core module and add the @org.springframework.stereotype.Service annotation to it with the name specified in the interface:

    package com.sample.sales.core;
    
    import com.sample.sales.entity.Order;
    import org.springframework.stereotype.Service;
    
    @Service(OrderService.NAME)
    public class OrderServiceBean implements OrderService {
        @Override
        public void calculateTotals(Order order) {
        }
    }

The service class, being a managed bean, should be placed inside the package tree with the root specified in the context:component-scan element of the spring.xml file. In this case, the spring.xml file contains the element:

<context:component-scan base-package="com.sample.sales"/>

which means that the search for annotated beans for this application block will be performed starting with the com.sample.sales package.

If different services or other Middleware components require calling the same business logic, it should be extracted and encapsulated inside an appropriate managed bean. For example:

// service interface
public interface SalesService {
    String NAME = "sample_SalesService";

    BigDecimal calculateSales(UUID customerId);
}
// service implementation
@Service(SalesService.NAME)
public class SalesServiceBean implements SalesService {

    @Inject
    private SalesCalculator salesCalculator;

    @Transactional
    @Override
    public BigDecimal calculateSales(UUID customerId) {
        return salesCalculator.calculateSales(customerId);
    }
}
// managed bean encapsulating business logic
@Component
public class SalesCalculator {

    @Inject
    private Persistence persistence;

    public BigDecimal calculateSales(UUID customerId) {
        Query query = persistence.getEntityManager().createQuery(
                "select sum(o.amount) from sample$Order o where o.customer.id = :customerId");
        query.setParameter("customerId", customerId);
        return (BigDecimal) query.getFirstResult();
    }
}
5.4.1.2. Using a Service

In order to call a service, the corresponding proxy object should be created in the client block of the application. There is a special factory that creates service proxies: for the Web Client block, it is WebRemoteProxyBeanCreator, for Web Portal – PortalRemoteProxyBeanCreator, for Desktop Client – RemoteProxyBeanCreator.

The proxy object factory is configured in spring.xml of the corresponding client block and contains service names and interfaces.

For example, to call the sales_OrderService service from the web client in the sales application, add the following code into the web-spring.xml file of the web module:

<bean id="sales_proxyCreator" class="com.haulmont.cuba.web.sys.remoting.WebRemoteProxyBeanCreator">
    <property name="serverSelector" ref="cuba_ServerSelector"/>
    <property name="remoteServices">
        <map>
            <entry key="sales_OrderService" value="com.sample.sales.core.OrderService"/>
        </map>
    </property>
</bean>

All imported services should be declared in the single remoteServices property in the map/entry elements.

Tip

CUBA Studio automatically registers services in all client blocks of the project.

From the application code perspective, the service’s proxy object at the client level is a standard Spring bean and can be obtained either by injection or through AppBeans class. For example:

@Inject
private OrderService orderService;

public void calculateTotals() {
    orderService.calculateTotals(order);
}

or

public void calculateTotals() {
    AppBeans.get(OrderService.class).calculateTotals(order);
}
5.4.1.3. DataService

DataService provides a facade for calling DataManager middleware implementation from the client tier. The usage of DataService interface in the application code is not recommended. Instead, use DataManager directly on both middle and client tiers.

5.4.2. Data Stores

A usual way of working with data in CUBA applications is manipulating entities - either declaratively through datasources and data-aware visual components, or programmatically via DataManager or EntityManager. The entities are mapped to data in a data store, which is usually a relational database. An application can connect to multiple data stores so its data model will contain entities mapped to data located in different databases.

An entity can belong only to a single data store. You can display entities from different data stores on a single UI screen, and DataManager will ensure they will be dispatched to appropriate data stores on save. Depending on the entity type, DataManager selects a registered data store represented by an implementation of the DataStore interface and delegates loading and saving entities to it. When you control transactions in your code and work with entities via EntityManager, you have to specify explicitly what data store to use. See the Persistence interface methods and @Transactional annotation parameters for details.

The platform contains a single implementation of the DataStore interface called RdbmsStore. It is designed to work with relational databases through the ORM layer. You can implement DataStore in your project to provide integration, for example, with a non-relational database or an external system having REST interface.

In any CUBA application, there is always the main data store which contains system and security entities and where the users log in. When we mention a database in this manual, we always mean the main data store if not explicitly stated otherwise. The main data store must be a relational database connected through a JDBC data source. The main data source is located in JNDI and should have a name specified in the cuba.dataSourceJndiName application property, which is jdbc/CubaDS by default.

Additional data store names should be specified in the cuba.additionalStores application property. If the additional store is RdbmsStore, you should provide the following properties for it:

  • cuba.dataSourceJndiName_{store_name} - JNDI name of the corresponding JDBC data source.

  • cuba.dbmsType_{store_name} - type of the data store DBMS.

  • cuba.persistenceConfig_{store_name} - location of the data store persistence.xml file.

If you implement the DataStore interface in your project, specify the name of the implementation bean in the cuba.storeImpl_{store_name} application property.

For example, if you need to work with two additional data stores: db1 (a PostgreSQL database) and mem1 (an in-memory storage implemented by some project bean), specify the following application properties in the app.properties file of your core module:

cuba.additionalStores = db1, mem1
cuba.dataSourceJndiName_db1 = jdbc/db1
cuba.dbmsType_db1 = postgres
cuba.persistenceConfig_db1 = com/company/sample/db1-persistence.xml
cuba.storeImpl_mem1 = sample_InMemoryStore

The cuba.additionalStores and cuba.persistenceConfig_db1 properties should also be specified in the property files of all used application blocks (web-app.properties, portal-app.properties, etc.).

Tip

CUBA Studio allows you to set up additional data stores on the Project properties > Advanced tab. It automatically creates all required application properties and JDBC data sources, as well as maintains additional persistence.xml files. After that, you can select a data store for an entity in the Data store field of the entity designer. You will also be able to select a data store when using the Generate model wizard for creation of new entities mapped to an existing database schema.

References between entities from different data stores

DataManager can automatically maintain TO-ONE references between entities from different data stores, if they are properly defined. For example, consider the case when you have Order entity in the main data store and Customer entity in an additional data store, and you want to have a reference from Order to Customer. Then do the following:

  • In the Order entity, define an attribute with the type of the Customer identifier. The attribute should be annotated with @SystemLevel to exclude it from various lists available to users, like attributes in Filter:

    @SystemLevel
    @Column(name = "CUSTOMER_ID")
    private Long customerId;
  • In the Order entity, define a non-persistent reference to Customer and specify the customerId attribute as "related":

    @Transient
    @MetaProperty(related = "customerId")
    private Customer customer;
  • Include non-persistent customer attribute to appropriate views.

After that, when you load Order with a view including customer attribute, DataManager automatically loads related Customer from the additional data store. The loading of collections is optimized for performance: after loading a list of orders, the loading of references from the additional data store is done in batches. The size of the batch is defined by the cuba.crossDataStoreReferenceLoadingBatchSize application property.

When you commit an entity graph which includes Order with Customer, DataManager saves the instances via corresponding DataStore implementations, and then saves the identifier of the customer in the order’s customerId attribute.

Cross-datastore references are also supported by the Filter component.

Tip

CUBA Studio automatically maintains the set of attributes for cross-datastore references when you select an entity from a different data store as an association.

5.4.3. The Persistence Interface

The Persistence interface is designed to be an entry point to the data storage functionality provided by the ORM layer.

The interface has the following methods:

  • createTransaction(), getTransaction() – obtain the interface for managing transactions. The methods can accept a data store name. If it is not provided, the main data store is assumed.

  • callInTransaction(), runInTransaction() - execute an action in a new transaction with or without return value. The methods can accept a data store name. If it is not provided, the main data store is assumed.

  • isInTransaction() – checks if there is an active transaction the moment.

  • getEntityManager() – returns an EntityManager instance bound to the current transaction. The method can accept a data store name. If it is not provided, the main data store is assumed.

  • isSoftDeletion() – allows you to determine if the soft deletion mode is active.

  • setSoftDeletion() – enables or disables the soft deletion mode. Setting this property affects all newly created EntityManager instances. Soft deletion is enabled by default.

  • getDbTypeConverter() – returns the DbTypeConverter instance for the main database or for an additional data store.

  • getDataSource() – returns the javax.sql.DataSource instance for the main database or for an additional data store.

    Warning

    For all javax.sql.Connection objects obtained through getDataSource().getConnection() method the close() method should be called in the finally section after using the connection. Otherwise, the connection will not be returned to the pool. Over time, the pool will overflow and the application will not be able to execute database queries.

  • getTools() – returns an instance of the PersistenceTools interface (see below).

5.4.3.1. PersistenceTools

Managed bean containing helper methods related to data storage functionality. It can be obtained either by calling the Persistence.getTools() method or like any other bean, through injection or the AppBeans class.

The PersistenceTools bean has the following methods:

  • getDirtyFields() – returns a collection of entity attribute names that have been changed since the last load of the instance from the DB. For new instances an empty collection is returned.

  • isLoaded() – determines if the specified instance attribute was loaded from the DB. The attribute may not be loaded, if it was not present in the view specified when loading the instance.

    This method only works for instances in the Managed state.

  • getReferenceId() – returns an ID of the related entity without loading it from the DB.

    Let us suppose that an Order instance was loaded in the persistent context and it is necessary to get the ID value of the Customer instance related to this Order. A call to the order.getCustomer().getId() method will execute the DB query to load the Customer instance, which in this case is unnecessary, because the value of the Customer ID is also located in the Order table as a foreign key. Whereas the execution of

    persistence.getTools().getReferenceId(order, "customer")

    will not send any additional queries to the database.

    This method works only for instances in the Managed state.

The PersistenceTools bean can be overridden in your application to extend the set of default helper methods. An example of working with the extended interface is shown below:

MyPersistenceTools tools = persistence.getTools();
tools.foo();
((MyPersistenceTools) persistence.getTools()).foo();
5.4.3.2. DbTypeConverter

The interface containing methods for conversion between data model attribute values and parameters/results of JDBC queries. An object of this interface can be obtained through the Persistence.getDbTypeConverter() method.

The DbTypeConverter interface has the following methods:

  • getJavaObject() – converts the result of the JDBC query into a type suitable for assigning to entity attribute.

  • getSqlObject() – converts the value of the entity attribute into a type suitable for assigning to the JDBC query parameter.

  • getSqlType() – returns a java.sql.Types constant that corresponds to the passed entity attribute type.

5.4.4. ORM Layer

Object-Relational Mapping is the technology for linking relational database tables to programming language objects.

Benefits of using ORM
  • Enables working with a relational DBMS by means of Java objects manipulation.

  • Simplifies programming by eliminating routine writing of SQL queries.

  • Simplifies programming by letting you load and save entire object graphs with one command.

  • Ensures easy porting of the application to different DBMS.

  • Enables use of a concise object query language – JPQL.

Shortcomings
  • Requires understanding of how ORM works.

  • Makes direct optimization of SQL and use of the DBMS specifics difficult.

CUBA uses the ORM implementation based on the EclipseLink framework.

5.4.4.1. EntityManager

EntityManager – main ORM interface for working with persistent entities.

Tip

See DataManager vs. EntityManager for information on differences between EntityManager and DataManager.

Reference to EntityManager may be obtained via the Persistence interface by calling its getEntityManager() method. The retrieved instance of EntityManager is bound to the current transaction, i.e. all calls to getEntityManager() as part of one transaction return one and the same instance of EntityManager. After the end of the transaction using the corresponding EntityManager instance is impossible.

An instance of EntityManager contains a persistence context – a set of instances loaded from the database or newly created. The persistence context is a data cache within a transaction. EntityManager automatically flushes to the database all changes made in its persistence context on the transaction commit or when the EntityManager.flush() method is called.

The EntityManager interface used in CUBA applications mainly copies the standard javax.persistence.EntityManager interface. Let us have a look at its main methods:

  • persist() – adds a new instance of the entity to the persistence context. When the transaction is committed a corresponding record is created in DB using SQL INSERT.

  • merge() – copies the state of detached instance to the persistence context the following way: an instance with the same identifier gets loaded from DB and the state of the passed Detached instance is copied into it and then the loaded Managed instance is returned. After that you should work with the returned Managed instance. The state of this entity will be stored in DB using SQL UPDATE on transaction commit.

  • remove() – removes an object from the database, or, if soft deletion mode is turned on, sets deleteTs and deletedBy attributes.

    If the passed instance is in Detached state, merge() is performed first.

  • find() – loads an entity instance by its identifier.

    When forming a request to the database the system considers the view which has been passed as a parameter to this method. As a result, the persistence context will contain a graph of objects with all view attributes loaded.

  • createQuery() – creates a Query or TypedQuery object for executing a JPQL query.

  • createNativeQuery() – creates a Query object to execute an SQL query.

  • reload() – reloads the entity instance with the provided view.

  • isSoftDeletion() – checks if the EntityManager is in soft deletion mode.

  • setSoftDeletion() – sets soft deletion mode for this EntityManager.

  • getConnection() – returns a java.sql.Connection, which is used by this instance of EntityManager, and hence by the current transaction. Such connection does not need to be closed, it will be closed automatically when the transaction is complete.

  • getDelegate() – returns javax.persistence.EntityManager provided by the ORM implementation.

Example of using EntityManager in a service:

@Service(SalesService.NAME)
public class SalesServiceBean implements SalesService {

    @Inject
    private Persistence persistence;

    @Override
    public BigDecimal calculateSales(UUID customerId) {
        BigDecimal result;
        // start transaction
        try (Transaction tx = persistence.createTransaction()) {
            // get EntityManager for the current transaction
            EntityManager em = persistence.getEntityManager();
            // create and execute Query
            Query query = em.createQuery(
                    "select sum(o.amount) from sample$Order o where o.customer.id = :customerId");
            query.setParameter("customerId", customerId);
            result = (BigDecimal) query.getFirstResult();
            // commit transaction
            tx.commit();
        }
        return result != null ? result : BigDecimal.ZERO;
    }
}
Partial entities

By default, in EntityManager, a view affects only reference attributes, i.e. all local attributes are loaded.

You can force EntityManager to load partial entities if you set the loadPartialEntities attribute of the view to true. If the loaded entity is cached, this view attribute is ignored and the entity will still be loaded with all local attributes.

5.4.4.2. Entity States
New

An instance which has just been created in memory: Car car = new Car().

A new instance may be passed to EntityManager.persist() to be stored to the database, in which case it changes its state to Managed.

Managed

An instance loaded from the database, or a new one passed to EntityManager.persist(). Belongs to a EntityManager instance, i.e. is contained in its persistence context.

Any changes of the instance in Managed state will be saved to the database when the transaction that the EntityManager belongs to is committed.

Detached

An instance loaded from the database and detached from its persistence context (as a result of the transaction end or serialization).

The changes applied to a Detached instance will be saved to the database only if this instance becomes Managed by being passed to EntityManager.merge().

5.4.4.3. Lazy Loading

Lazy loading (loading on demand) enables delayed loading of linked entities, i.e. they get loaded when their properties are accessed for the first time.

Lazy loading generates more database queries than eager fetching, but it is stretched in time.

  • For example, in case of lazy loading of a list of N instances of entity A, each containing a link to an instance of entity B, will require N+1 requests to DB.

  • In most cases, minimizing the number of requests to the database results in less response time and database load. The platform uses the mechanism of views to achieve this. Using view allows ORM to create only one request to the database with table joining for the above mentioned case.

Lazy loading works only for instances in Managed state, i.e. within the transaction which loaded the instance.

5.4.4.4. Executing JPQL Queries

The Query interface is designed to execute JPQL queries. The reference to it may be obtained from the current EntityManager instance by calling createQuery() method. If the query is supposed to be used to load entities, we recommend calling createQuery() with the result type as a parameter. This will create a TypedQuery instance.

The methods of Query mainly correspond to the methods of the standard JPA javax.persistence.Query interface. Let us have a look at the differences.

  • setParameter() – sets a value to a query parameter. If the value is an entity instance, implicitly converts the instance into its identifier. For example:

    Customer customer = ...;
    TypedQuery<Order> query = entityManager.createQuery(
        "select o from sales$Order o where o.customer.id = ?1", Order.class);
    query.setParameter(1, customer);

    Note that the entity is passed as a parameter while comparison in the query is done using identifier.

    A variant of the method with implicitConversions = false does not perform such conversion.

  • setView(), addView() – define a view which is used to load data.

  • getDelegate() – returns an instance of javax.persistence.Query, provided by the ORM implementation.

If a view is set for a query, then by default the query has FlushModeType.AUTO, which affects the case when the current persistence context contains changed entity instances: these instances will be saved to the database prior to the query execution. In other words, ORM first synchronizes the state of entities in the persistence context and in the database, and only after that runs the query. It guarantees that the query results contain all relevant instances, even if they have not been saved to the database explicitly yet. The downside of this is that you will have an implicit flush, i.e. execution of SQL update statements for all currently changed entity instances, which may affect performance.

If a query is executed without a view, then by default the query has FlushModeType.COMMIT, which means that the query will not cause a flush, and the query results will not respect the contents of the current persistence context.

In most cases ignoring the current persistence context is acceptable, and is a preferred behavior because it doesn’t lead to extra SQL updates. But there is the following issue when using views: if there is a changed entity instance in the persistence context, and you execute a query with a view and FlushModeType.COMMIT loading the same instance, the changes will be lost. That is why we use FlushModeType.AUTO by default when running queries with views.

You can also set flush mode explicitly using the setFlushMode() method of the Query interface. It will override the default settings described above.

5.4.4.4.1. JPQL Functions

The table below describes the JPQL functions supported and not supported by CUBA Platform.

Function Support Query

Aggregate Functions

Supported

SELECT AVG(o.quantity) FROM app$Order o

Not supported: aggregate functions with scalar expression (EclipseLink feature)

SELECT AVG(o.quantity)/2.0 FROM app$Order o

SELECT AVG(o.quantity * o.price) FROM app$Order o

ALL, ANY, SOME

Supported

SELECT emp FROM app$Employee emp WHERE emp.salary > ALL (SELECT m.salary FROM app$Manager m WHERE m.department = emp.department)

Arithmetic Functions (INDEX, SIZE, ABS, SQRT, MOD)

Supported

SELECT w.name FROM app$Course c JOIN c.studentWaitlist w WHERE c.name = 'Calculus' AND INDEX(w) = 0

SELECT w.name FROM app$Course c WHERE c.name = 'Calculus' AND SIZE(c.studentWaitlist) = 1

SELECT w.name FROM app$Course c WHERE c.name = 'Calculus' AND ABS(c.time) = 10

SELECT w.name FROM app$Course c WHERE c.name = 'Calculus' AND SQRT(c.time) = 10.5

SELECT w.name FROM app$Course c WHERE c.name = 'Calculus' AND MOD(c.time, c.time1) = 2

CASE Expressions in UPDATE query

Supported

SELECT e.name, f.name, CONCAT(CASE WHEN f.annualMiles > 50000 THEN 'Platinum ' WHEN f.annualMiles > 25000 THEN 'Gold ' ELSE '' END, 'Frequent Flyer') FROM app$Employee e JOIN e.frequentFlierPlan f

Not supported: CASE in UPDATE query

UPDATE app$Employee e SET e.salary = CASE e.rating WHEN 1 THEN e.salary * 1.1 WHEN 2 THEN e.salary * 1.05 ELSE e.salary * 1.01 END

Date Functions (CURRENT_DATE, CURRENT_TIME, CURRENT_TIMESTAMP)

Supported

SELECT e FROM app$Order e WHERE e.date = CURRENT_DATE

EclipseLink Functions (CAST, REGEXP, EXTRACT)

Supported

SELECT EXTRACT(YEAR FROM e.createTs) FROM app$MyEntity e WHERE EXTRACT(YEAR FROM e.createTs) > 2012

SELECT e FROM app$MyEntity e WHERE e.name REGEXP '.*'

SELECT CAST(e.number text) FROM app$MyEntity e WHERE e.path LIKE CAST(:ds$myEntityDs.id text)

Not supported: CAST in GROUP BY clause

SELECT e FROM app$Order e WHERE e.amount > 100 GROUP BY CAST(e.orderDate date)

Entity Type Expression

Supported: entity type passed as a parameter

SELECT e FROM app$Employee e WHERE TYPE(e) IN (:empType1, :empType2)

Not supported: direct link to an entity type

SELECT e FROM app$Employee e WHERE TYPE(e) IN (app$Exempt, app$Contractor)

Function Invocation

Supported: function result in comparison clauses

SELECT u FROM sec$User u WHERE function('DAYOFMONTH', u.createTs) = 1

Not supported: function result as is

SELECT u FROM sec$User u WHERE function('hasRoles', u.createdBy, u.login)

IN

Supported

SELECT e FROM Employee e, IN(e.projects) p WHERE p.budget > 1000000

IS EMPTY collection

Supported

SELECT e FROM Employee e WHERE e.projects IS EMPTY

KEY/VALUE

Not supported

SELECT v.location.street, KEY(i).title, VALUE(i) FROM app$VideoStore v JOIN v.videoInventory i WHERE v.location.zipcode = '94301' AND VALUE(i) > 0

Literals

Supported

SELECT e FROM app$Employee e WHERE e.name = 'Bob'

SELECT e FROM app$Employee e WHERE e.id = 1234

SELECT e FROM app$Employee e WHERE e.id = 1234L

SELECT s FROM app$Stat s WHERE s.ratio > 3.14F

SELECT s FROM app$Stat s WHERE s.ratio > 3.14e32D

SELECT e FROM app$Employee e WHERE e.active = TRUE

Not supported: date and time literals

SELECT e FROM app$Employee e WHERE e.startDate = {d'2012-01-03'}

SELECT e FROM app$Employee e WHERE e.startTime = {t'09:00:00'}

SELECT e FROM app$Employee e WHERE e.version = {ts'2012-01-03 09:00:00.000000001'}

MEMBER OF

Supported: fields or query results

SELECT d FROM app$Department d WHERE (select e from app$Employee e where e.id = :eParam) MEMBER OF e.employees

Not supported: literals

SELECT e FROM app$Employee e WHERE 'write code' MEMBER OF e.codes

NEW in SELECT

Not supported

SELECT NEW com.acme.example.CustomerDetails(c.id, c.status, o.count) FROM app$Customer c JOIN c.orders o WHERE o.count > 100

NULLIF/COALESCE

Supported

SELECT NULLIF(emp.salary, 10) FROM app$Employee emp

SELECT COALESCE(emp.salary, emp.salaryOld, 10) FROM app$Employee emp

NULLS FIRST, NULLS LAST in order by

Supported

SELECT h FROM sec$GroupHierarchy h ORDER BY h.level DESC NULLS FIRST

String Functions (CONCAT, SUBSTRING, TRIM, LOWER, UPPER, LENGTH, LOCATE)

Supported

SELECT x FROM app$Magazine x WHERE CONCAT(x.title, 's') = 'JDJs'

SELECT x FROM app$Magazine x WHERE SUBSTRING(x.title, 1, 1) = 'J'

SELECT x FROM app$Magazine x WHERE LOWER(x.title) = 'd'

SELECT x FROM app$Magazine x WHERE UPPER(x.title) = 'D'

SELECT x FROM app$Magazine x WHERE LENGTH(x.title) = 10

SELECT x FROM app$Magazine x WHERE LOCATE('A', x.title, 4) = 6

SELECT x FROM app$Magazine x WHERE TRIM(TRAILING FROM x.title) = 'D'

Not supported: TRIM with trim char

SELECT x FROM app$Magazine x WHERE TRIM(TRAILING 'J' FROM x.title) = 'D'

Subquery

Supported

SELECT goodCustomer FROM app$Customer goodCustomer WHERE goodCustomer.balanceOwed < (SELECT AVG(c.balanceOwed) FROM app$Customer c)

Not supported: path expression instead of entity name in subquery’s FROM

SELECT c FROM app$Customer c WHERE (SELECT AVG(o.price) FROM c.orders o) > 100

TREAT

Supported

SELECT e FROM app$Employee e JOIN TREAT(e.projects AS app$LargeProject) p WHERE p.budget > 1000000

Not supported: TREAT in WHERE clauses

SELECT e FROM Employee e JOIN e.projects p WHERE TREAT(p as LargeProject).budget > 1000000

5.4.4.4.2. Case-Insensitive Substring Search

You can use the (?i) prefix in the value of the query parameters to conveniently specify conditions for case insensitive search by any part of the string. For example, look at the query:

select c from sales$Customer c where c.name like :name

If you pass the string (?i)%doe% as a value of the name parameter, the search will return John Doe, if such record exists in the database, even though the case of symbols is different. This will happen because ORM will run the SQL query with the condition lower(C.NAME) like ?.

It should be kept in mind that such search will not use index on the name field, even if such exists in the database.

5.4.4.4.3. Macros in JPQL

JPQL query text can include macros, which are processed before the query is executed. They are converted into the executable JPQL and can additionally modify the set of query parameters.

The macros solve the following problems:

  • Provide a workaround for the limitation of JPQL which makes it impossible to express the condition of dependency of a given field on current time (i.e. expressions like "current_date -1" do not work).

  • Enable comparing Timestamp type fields (the date/time fields) with a date.

Let us consider them in more detail:

@between

Has the format @between(field_name, moment1, moment2, time_unit), where

  • field_name is the name of the compared attribute.

  • moment1, moment2 – start and end points of the time interval where the value of field_name should fall into. Each of the points should be defined by an expression containing now variable with an addition or subtraction of an integer number.

  • time_unit – defines the unit for time interval added to or subtracted from now in the time point expressions and time points rounding precision. May be one of the following: year, month, day, hour, minute, second.

The macro gets converted to the following expression in JPQL: field_name >= :moment1 and field_name < :moment2

Example 1. Customer was created today:

select c from sales$Customer where @between(c.createTs, now, now+1, day)

Example 2. Customer was created within the last 10 minutes:

select c from sales$Customer where @between(c.createTs, now-10, now, minute)

Example 3. Documents dated within the last 5 work days (for the projects including workflow):

select d from sales$Doc where @between(d.createTs, now-5, now, workday)
@today

Has the format @today(field_name) and helps to define a condition checking that the attribute value falls into the current date. Essentially, this is a special case of the @between macro.

Example. Customer was created today:

select d from sales$Doc where @today(d.createTs)
@dateEquals

Has the format @dateEquals(field_name, parameter) and allows you to define a condition checking that field_name value (in Timestamp format) falls into the date passed as parameter.

Example:

select d from sales$Doc where @dateEquals(d.createTs, :param)
@dateBefore

Has the format @dateBefore(field_name, parameter) and allows you to define a condition checking that field_name value (in Timestamp format) is smaller than the date passed as parameter.

Example:

select d from sales$Doc where @dateBefore(d.createTs, :param)
@dateAfter

Has the format @dateAfter(field_name, parameter) and allows you to define a condition that the date of the field_name value (in Timestamp format) is more or equal to the date passed as parameter.

Example:

select d from sales$Doc where @dateAfter(d.createTs, :param)
@enum

Allows you to use a fully qualified enum constant name instead of its database identifier. This simplifies searching for enum usages throughout the application code.

Example:

select r from sec$Role where r.type = @enum(com.haulmont.cuba.security.entity.RoleType.SUPER) order by r.name
5.4.4.5. Running SQL Queries

ORM enables execution of SQL queries returning either the lists of individual fields or entity instances. To do this, create a Query or TypedQuery object by calling one of the EntityManager.createNativeQuery() methods.

If individual columns are selected, the resulting list will include the rows as Object[]. For example:

Query query = persistence.getEntityManager().createNativeQuery(
        "select ID, NAME from SALES_CUSTOMER where NAME like ?1");
query.setParameter(1, "%Company%");
List list = query.getResultList();
for (Iterator it = list.iterator(); it.hasNext(); ) {
    Object[] row = (Object[]) it.next();
    UUID id = (UUID) row[0];
    String name = (String) row[1];
}

If a single column or aggregate function is selected, the result list will contain these values directly:

Query query = persistence.getEntityManager().createNativeQuery(
        "select count(*) from SEC_USER where login = #login");
query.setParameter("login", "admin");
long count = (long) query.getSingleResult();

If the resulting entity class is passed to EntityManager.createNativeQuery() along with the query text, TypedQuery is returned, and ORM attempts to map the query results to entity attributes. For example:

TypedQuery<Customer> query = em.createNativeQuery(
    "select * from SALES_CUSTOMER where NAME like ?1",
    Customer.class);
query.setParameter(1, "%Company%");
List<Customer> list = query.getResultList();

Keep in mind when using SQL, that the columns corresponding to entity attributes of UUID type are returned as UUID or as String depending on the DBMS in use:

  • HSQLDBString

  • PostgreSQLUUID

  • Microsoft SQL ServerString

  • OracleString

  • MySQLString

Parameters of this type should also be passed either as UUID or using their string representation, depending on the DBMS. To ensure that your code does not depend on the DBMS specifics, use DbTypeConverter. It provides methods to convert data between Java objects and JDBC parameters and results.

Native queries support positional and named parameters. Positional parameters are marked in the query text with ? followed by the parameter number starting from 1. Named parameters are marked with the number sign (#). See the examples above.

Behavior of SQL queries returning entities and modifying queries (update, delete) in relation to the current persistence context is similar to that of JPQL queries described above.

5.4.4.6. Entity Listeners

Entity Listeners are designed to react to the entity instances lifecycle events on Middleware.

A listener is a class implementing one or several interfaces from the com.haulmont.cuba.core.listener package. The listener will react to events corresponding to the implemented interfaces.

BeforeDetachEntityListener

onBeforeDetach() method is called before the object is detached from EntityManager on transaction commit.

This listener can be used for populating non-persistent entity attributes before sending it to the client tier.

BeforeAttachEntityListener

onBeforeAttach() method is called before the object is attached to the persistence context as a result of EntityManager.merge() operation.

This listener can be used, for example, to populate persistent entity attributes before saving it to the database.

BeforeInsertEntityListener

onBeforeInsert() method is called before a record is inserted into the database. All kinds of operations can be performed with the current EntityManager available within this method.

AfterInsertEntityListener

onAfterInsert() is called after a record is inserted into database, but before transaction commit. This method does not allow modifications of the current persistence context, however, database modifications can be done using QueryRunner.

BeforeUpdateEntityListener

onBeforeUpdate() method is called before a record is updated in the database. All kinds of operations can be performed with the current EntityManager available within this method.

AfterUpdateEntityListener

onAfterUpdate() method is called after a record was updated in the database, but before transaction commit. This method does not allow modifications of the current persistence context, however, database modifications can be done using QueryRunner.

BeforeDeleteEntityListener

onBeforeDelete() method is called before a record is deleted from the database (in the case of soft deletion – before updating a record). All kinds of operations can be performed with the current EntityManager available within this method.

AfterDeleteEntityListener

onAfterDelete() method is called after a record is deleted from the database (in the case of soft deletion – before updating a record), but before transaction commit. This method does not allow modifications of the current persistence context, however, database modifications can be done using QueryRunner.

An entity listener should be a managed bean, so you can use injection. For example:

@Component("sample_MyEntityListener")
public class MyEntityListener implements
        BeforeInsertEntityListener<MyEntity>,
        BeforeUpdateEntityListener<MyEntity> {

    @Inject
    protected Metadata metadata;

    @Override
    public void onBeforeInsert(MyEntity entity, EntityManager entityManager) {
        Foo foo = metadata.create(Foo.class);
        ...
        entity.setFoo(foo);
        entityManager.persist(foo);
    }

    @Override
    public void onBeforeUpdate(MyEntity entity, EntityManager entityManager) {
        Foo foo = entityManager.find(Foo.class, entity.getFoo().getId());
        ...
    }
}

An entity listener can be specified for an entity in two ways:

  • Statically – the bean names of listeners are listed in @Listeners annotation on the entity class.

    @Entity(...)
    @Table(...)
    @Listeners("sample_MyEntityListener")
    public class MyEntity extends StandardEntity {
        ...
    }
  • Dynamically – the bean name of the listener is passed to the addListener() method of the EntityListenerManager bean. See an example in this section: Running Code on Startup.

Only one listener instance of a certain type is created for all instances of a particular entity class, therefore listener must not have a state.

If several listeners of the same type (for example from annotations of entity class and its parents and also added dynamically) were declared for an entity, they will be invoked in the following order:

  1. For each ancestor, starting from the most distant one, dynamically added listeners are invoked first, followed by statically assigned listeners.

  2. Once parent classes are processed, dynamically added listeners for the given class are invoked first, followed by statically assigned.

Tip

If you need to do some calculations and update attributes of multiple entity instances at once on transaction commit, consider using a transaction listener.

5.4.5. Transaction Management

This section covers various aspects of transaction management in CUBA applications.

5.4.5.1. Programmatic Transaction Management

Programmatic transaction management is done using the com.haulmont.cuba.core.Transaction interface. A reference to it can be obtained via the createTransaction() or getTransaction() methods of the Persistence infrastructure interface.

The createTransaction() method creates a new transaction and returns the Transaction interface. Subsequent calls of commit(), commitRetaining(), end() methods of this interface control the created transaction. If at the moment of creation there was another transaction, it will be suspended and resumed after the completion of the newly created one.

The getTransaction() method either creates a new transaction or attaches to an existing one. If at the moment of the call there is an active transaction, then the method completes successfully, but subsequent calls of commit(), commitRetaining(), end() have no influence on the existing transaction. However calling end() without a prior call to commit() will mark current transaction as RollbackOnly.

Examples of programmatic transaction management:

@Inject
private Metadata metadata;
@Inject
private Persistence persistence;
...
// try-with-resources style
try (Transaction tx = persistence.createTransaction()) {
    Customer customer = metadata.create(Customer.class);
    customer.setName("John Smith");
    persistence.getEntityManager().persist(customer);
    tx.commit();
}
// plain style
Transaction tx = persistence.createTransaction();
try {
    Customer customer = metadata.create(Customer.class);
    customer.setName("John Smith");
    persistence.getEntityManager().persist(customer);
    tx.commit();
} finally {
    tx.end();
}

Transaction interface has also the execute() method accepting an action class or a lambda expression. The action will be performed in the transaction. This enables organizing transaction management in functional style, for example:

UUID customerId = persistence.createTransaction().execute((EntityManager em) -> {
    Customer customer = metadata.create(Customer.class);
    customer.setName("ABC");
    em.persist(customer);
    return customer.getId();
});

Customer customer = persistence.createTransaction().execute(em ->
        em.find(Customer.class, customerId, "_local"));

Keep in mind that execute() method of a given instance of Transaction may be called only once because the transaction ends after the action code is executed.

5.4.5.2. Declarative Transaction Management

Any method of the Middleware managed bean may be annotated with @org.springframework.transaction.annotation.Transactional, which will automatically create a transaction when the method is called. Such method does not require invoking Persistence.createTransaction(), you can immediately get EntityManager and work with it.

@Transactional annotation supports a number of parameters, including:

  • propagation - transaction creation mode. The REQUIRED value corresponds to getTransaction(), the REQUIRES_NEW value – to createTransaction(). The default value is REQUIRED.

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void doSomething() {
    }
  • value - data store name. If omitted, the main data store is assumed. For example:

    @Transactional("db1")
    public void doSomething() {
    }

Declarative transaction management allows you to reduce the amount of boilerplate code, but it has the following drawback: transactions are committed outside of the application code, which often complicates debugging because it conceals the moment when changes are sent to the database and the entities become Detached. Additionally, keep in mind that declarative markup will only work if the method is called by the container, i.e. calling a transaction method from another method of the same object will not start a transaction.

With this in mind, we recommend using declarative transaction management only for simple cases like a service method reading a certain object and returning it to the client.

5.4.5.3. Examples of Transactions Interaction
Rollback of a Nested Transaction

If a nested transaction was created via getTransaction() and rolled back, then commit of the enclosing transaction will be impossible. For example:

void methodA() {
    Transaction tx = persistence.createTransaction();
    try {
        // (1) calling a method creating a nested transaction
        methodB();

        // (4) at this point an exception will be thrown, because transaction
        // is marked as rollback only
        tx.commit();
    } finally {
        tx.end();
    }
}

void methodB() {
    Transaction tx = persistence.getTransaction();
    try {
        // (2) let us assume the exception occurs here
        tx.commit();
    } catch (Exception e) {
        // (3) handle it and exit
        return;
    } finally {
        tx.end();
    }
}

If the transaction in methodB() is created with createTransaction() instead, then rolling it back will have no influence on the enclosing transaction in methodA().

Reading and Modifying Data in a Nested Transaction

Let us first have a look at a dependent nested transaction created using getTransaction():

void methodA() {
    Transaction tx = persistence.createTransaction();
    try {
        EntityManager em = persistence.getEntityManager();

        // (1) loading an entity with name == "old name"
        Employee employee = em.find(Employee.class, id);
        assertEquals("old name", employee.getName());

        // (2) setting new value to the field
        employee.setName("name A");

        // (3) calling a method creating a nested transaction
        methodB();

        // (8) the changes are committed to DB, and
        // it will contain "name B"
        tx.commit();
    } finally {
      tx.end();
    }
}

void methodB() {
    Transaction tx = persistence.getTransaction();
    try {
        // (4) retrieving the same instance of EntityManager as methodA
        EntityManager em = persistence.getEntityManager();

        // (5) loading an entity with the same identifier
        Employee employee = em.find(Employee.class, id);

        // (6) the field value is the new one since we are working with the same
        // persistent context, and there are no calls to DB at all
        assertEquals("name A", employee.getName());
        employee.setName("name B");

        // (7) no actual commit is done at this point
        tx.commit();
    } finally {
      tx.end();
    }
}

Now, let us have a look at the same example with an independent nested transaction created with createTransaction():

void methodA() {
    Transaction tx = persistence.createTransaction();
    try {
        EntityManager em = persistence.getEntityManager();

        // (1) loading an entity with name == "old name"
        Employee employee = em.find(Employee.class, id);
        assertEquals("old name", employee.getName());

        // (2) setting new value to the field
        employee.setName("name A");

        // (3) calling a method creating a nested transaction
        methodB();

        // (8) an exception occurs due to optimistic locking
        // and commit will fail
        tx.commit();
    } finally {
      tx.end();
    }
}

void methodB() {
    Transaction tx = persistence.createTransaction();
    try {
        // (4) creating a new instance of EntityManager,
        // as this is a new transaction
        EntityManager em = persistence.getEntityManager();

        // (5) loading an entity with the same identifier
        Employee employee = em.find(Employee.class, id);

        // (6) the field value is old because an old instance of the entity
        // has been loaded from DB
        assertEquals("old name", employee.getName());

        employee.setName("name B");

        // (7) the changes are commited to DB, and the value of
        // "name B" will now be in DB
        tx.commit();
    } finally {
      tx.end();
    }
}

In the last example, the exception at point (8) will only occur if the entity supports optimistic locking, i.e. if it implements Versioned interface.

5.4.5.4. Transaction Parameters
Transaction Timeout

You can set a timeout in seconds for created transaction. When the timeout is exceeded, the transaction is interrupted and rolled back. Transaction timeout effectively limits the maximum duration of a database request.

When transactions are managed programmatically, the timeout is specified by passing TransactionParams object to the Persistence.createTransaction() method. For example:

Transaction tx = persistence.createTransaction(new TransactionParams().setTimeout(2));

In case of declarative transactions management, use the timeout parameter of the @Transactional annotation:

@Transactional(timeout = 2)
public void someServiceMethod() {
...

The default timeout can be defined using the cuba.defaultQueryTimeoutSec application property.

Read-only Transactions

A transaction can be marked as read-only if it is intended only for reading data from the database. For example, all load methods of DataManager use read-only transactions by default. Read-only transactions yield better performance because the platform does not execute code that handles possible entity modifications. BeforeCommit transaction listeners are not invoked as well.

Warning

If the persistence context of a read-only transaction contains modified entities, IllegalStateException will be thrown on attempt to commit the transaction. It means that you should mark a transaction as read-only only when you are sure that it doesn’t modify any entity.

When transactions are managed programmatically, the read-only sign is specified by passing TransactionParams object to the Persistence.createTransaction() method. For example:

Transaction tx = persistence.createTransaction(new TransactionParams().setReadOnly(true));

In case of declarative transactions management, use the readOnly parameter of the @Transactional annotation:

@Transactional(readOnly = true)
public void someServiceMethod() {
...
5.4.5.5. Transaction Listeners

Transaction listeners are designed to react on transaction lifecycle events. Unlike entity listeners, they are not tied to an entity type and invoked for each transaction.

A listener is a managed bean implementing one or both BeforeCommitTransactionListener and AfterCompleteTransactionListener interfaces.

BeforeCommitTransactionListener

beforeCommit() method is called before transaction commit after all entity listeners if the transaction is not read-only. The method accepts a current EntityManager and a collection of entities in the current persistence context.

The listener can be used to enforce complex business rules involving multiple entities. In the following example, the amount attribute of the Order entity must be calculated based on discount value located in the order, and price and quantity of OrderLine entities constituted the order.

@Component("demo_OrdersTransactionListener")
public class OrdersTransactionListener implements BeforeCommitTransactionListener {

    @Inject
    private PersistenceTools persistenceTools;

    @Override
    public void beforeCommit(EntityManager entityManager, Collection<Entity> managedEntities) {
        // gather all orders affected by changes in the current transaction
        Set<Order> affectedOrders = new HashSet<>();

        for (Entity entity : managedEntities) {
            // skip not modified entities
            if (!persistenceTools.isDirty(entity))
                continue;

            if (entity instanceof Order)
                affectedOrders.add((Order) entity);
            else if (entity instanceof OrderLine) {
                Order order = ((OrderLine) entity).getOrder();
                // a reference can be detached, so merge it into current persistence context
                affectedOrders.add(entityManager.merge(order));
            }
        }
        // calculate amount for each affected order by its lines and discount
        for (Order order : affectedOrders) {
            BigDecimal amount = BigDecimal.ZERO;
            for (OrderLine orderLine : order.getOrderLines()) {
                if (!orderLine.isDeleted()) {
                    amount = amount.add(orderLine.getPrice().multiply(orderLine.getQuantity()));
                }
            }
            BigDecimal discount = order.getDiscount().divide(BigDecimal.valueOf(100), 2, BigDecimal.ROUND_DOWN);
            order.setAmount(amount.subtract(amount.multiply(discount)));
        }
    }
}
AfterCompleteTransactionListener

afterComplete() method is called after transaction is completed. The method accepts a parameter indicating whether the transaction was successfully committed and a collection of detached entities contained in the persistence context of the completed transaction.

Usage example:

@Component("demo_OrdersTransactionListener")
public class OrdersTransactionListener implements AfterCompleteTransactionListener {

    private Logger log = LoggerFactory.getLogger(OrdersTransactionListener.class);

    @Override
    public void afterComplete(boolean committed, Collection<Entity> detachedEntities) {
        if (!committed)
            return;

        for (Entity entity : detachedEntities) {
            if (entity instanceof Order) {
                log.info("Order: " + entity);
            }
        }
    }
}

5.4.6. Entity and Query Cache

Entity Cache

Entity cache is provided by EclipseLink ORM framework. It stores recently read or written entity instance in memory, which minimizes database access and improves the application performance.

Entity cache is used only when you retrieve entities by ID, so queries by other attributes still run on the database. However, these queries can be simpler and faster if related entities are in cache. For example, if you query for Orders together with related Customers and do not use cache, the SQL query will contain a JOIN for customers table. If Customer entities are cached, the SQL query will select only orders, and related customers will be retrieved from the cache.

In order to turn on entity cache, set the following properties in the app.properties file of your core module:

  • eclipselink.cache.shared.sales$Customer = true - turns on caching of sales$Customer entity.

  • eclipselink.cache.size.sales$Customer = 500 - sets cache size for sales$Customer to 500 instances. Default size is 100.

The fact of whether an entity is cached affects the fetch mode chosen by the platform for loading entity graphs. If a reference attribute is a cacheable entity, the fetch mode is always UNDEFINED, which allows ORM to retrieve the reference from the cache instead of executing queries with JOINs or separate batch queries.

The platform provides entity cache coordination in middleware cluster. When a cached entity is updated or deleted on one cluster node, the same cached instance on other nodes (if any) will be invalidated, so the next operation with this instance will read a fresh state from the database.

Query Cache

Query cache stores identifiers of entity instances returned by JPQL queries, so it naturally complements the entity cache.

For example, if entity cache is enabled for an entity (say, sales$Customer), and you execute the query select c from sales$Customer c where c.grade = :grade for the first time, the following happens:

  • ORM runs the query on the database.

  • Loaded Customer instances are placed to the entity cache.

  • A mapping of the query text and parameters to the list of identifiers of the returned instances is placed to the query cache.

When you execute the same query with the same parameters the second time, the platform finds the query in the query cache and loads entity instances from the entity cache by identifiers. No database operations are needed.

Queries are not cached by default. You can specify that a query should be cached on different layers of the application:

  • Using setCacheable() method of the Query interface when working with EntityManager.

  • Using setCacheable() method of the LoadContext.Query interface when working with DataManager.

  • Using setCacheable() method of the CollectionDatasource interface or cacheable XML attribute when working with datasources.

Warning

Use cacheable queries only if entity cache is enabled for the returned entity. Otherwise on every query entity instances will be fetched from the database by their identifiers one by one.

Query cache is invalidated automatically when ORM performs creation, update or deletion of instances of the corresponding entities. The invalidation works across the middleware cluster.

The app-core.cuba:type=QueryCacheSupport JMX-bean can be used to monitor the cache state and to evict cached queries manually. For example, if you have modified an instance of the sales$Customer entity directly in the database, you should evict all cached queries for this entity using the evict() operation with sales$Customer argument.

The following application properties affect the query cache:

5.4.7. System Authentication

When executing user requests, the Middleware program code always has access to the information on the current user via the UserSessionSource interface. This is possible because the corresponding SecurityContext object is automatically set for the current thread when a request is received from the client tier.

However, there are situations when the current thread is not associated with any system user, for example, when calling a bean’s method from the scheduler, or via the JMX interface. In case the bean modifies entities in the database, it will require information on who is making changes, i.e., authentication.

This kind of authentication is called "system authentication" as it requires no user participation – the application middle layer simply creates or uses an existing user session and sets the corresponding SecurityContext object for the current thread.

The following methods can be used to provide the system authentication for a code block:

  • Make use of the com.haulmont.cuba.security.app.Authentication bean:

    @Inject
    protected Authentication authentication;
    ...
    authentication.begin();
    try {
        // authenticated code
    } finally {
        authentication.end();
    }
  • Add the @Authenticated annotation to the bean method:

    @Authenticated
    public String foo(String value) {
        // authenticated code
    }

The second case uses the Authentication bean implicitly, via the AuthenticationInterceptor object, which intercepts calls of all bean methods with the @Authenticated annotation.

In the examples above, the user session will be created on behalf of the user, whose login is specified in the cuba.jmxUserLogin application property. If authentication on behalf of another user is required, pass the login of the desired user to the begin() method of the first variant.

Warning

If current thread has an active user session assigned at the time of Authentication.begin() execution, it will not be replaced. Therefore the code will be executed with the existing session and the subsequent call to the end() method will not clear the thread.

For example, if a bean is in the same JVM as the Web Client block, to which the user is currently connected, the call of the JMX bean method from the Web Client built-in JMX console will be executed on behalf of the currently logged in user, regardless of the system authentication.

5.5. Generic User Interface

Generic user interface (Generic UI, GUI) subsystem allows you to create UI screens using XML and Java. The screens created using this approach work identically in both standard client blocks: Web Client and Desktop Client.

ClientStructure
Figure 13. The Structure of Generic User Interface

Main components of Generic UI screens are marked as green:

  • XML-descriptors – XML files containing information about datasources and screen layout.

  • Controllers – Java classes containing logic for screen initialization and handling of events generated by UI controls.

The code of application screens included in the gui module interacts with visual component interfaces (VCL Interfaces) implemented separately in the web and desktop modules of the cuba application component. For Web Client the implementation is based on the Vaadin framework, for Desktop Client on the Java Swing framework.

Visual Components Library (VCL)contains a large set of ready-to-use components.

Datasources mechanism provides a unified interface that ensures functioning of data-aware visual components.

Client’s infrastructure (Infrastructure) includes main application window, mechanisms for display and interaction of UI screens and means of interaction with the middleware.

5.5.1. Screens

A generic UI screen is defined by an XML-descriptor and a controller class. The descriptor has a link to the controller class.

In order to be able to invoke the screen from the main menu or from Java code (e.g. from controller of a different screen) the XML-descriptor should be registered in the project’s screens.xml file.

The main menu of an application is generated separately for the Web Client and the Desktop Client based on the menu.xml files, located in the project’s web and desktop modules.

5.5.1.1. Screen Types

This section describes the following basic types of screens:

5.5.1.1.1. Frame

Frames are reusable parts of screens. Frames are included in screens using the frame XML element.

A frame controller must extend the AbstractFrame class.

Tip

You can create a frame in Studio using the Blank frame template.

Below are the rules of interaction between a frame and its enclosing screen:

  • Frame components can be referenced from a screen using dot: frame_id.component_id

  • List of screen components can be obtained from a frame controller by invoking getComponent(component_id) method, but only if there is no component with the same name in the frame itself. I.e. frame components mask screen components.

  • Screen datasource can be obtained from a frame by invoking getDsContext().get(ds_id) method or injection, or using ds$ds_id in query, but only if the datasource with the same name is not declared in the frame itself (same as for components).

  • From a screen, frame datasource can be obtained only by iterating the getDsContext().getChildren() collection.

The screen commit causes commit of modified datasources of all frames included in the screen.

5.5.1.1.2. Simple Screen

Simple screens enable displaying and editing of arbitrary information including individual instances and lists of entities. Screens of this type have only the core functionality for opening in the application’s main window and working with datasources.

The controller of a simple screen must be inherited from the AbstractWindow class.

Tip

You can create a simple screen in Studio using the Blank screen template.

5.5.1.1.3. Lookup Screen

Lookup screens are designed to select and return instances or lists of entities. The standard LookupAction in visual components like PickerField and LookupPickerField invokes lookup screens to select related entities.

When a lookup screen is invoked by the openLookup() method, it contains a panel with the buttons for selection. When a user selects an instance or multiple instances, the lookup screen invokes the handler which was passed to it, thus returning results to the calling code. When being invoked by openWindow() method or, for example, from the main menu, the selection panel is not displayed, effectively transforming the lookup screen into a simple screen.

The controller of a lookup screen should be inherited from the AbstractLookup class. The lookupComponent attribute of the screen’s XML must point to a component (for example Table), from which the selected entity instance should be taken as a result of the lookup.

Tip

You can create a lookup screen for an entity in Studio using the Entity browser or Entity combined screen templates.

By default, the LookupAction uses a lookup screen registered in screens.xml with the {entity_name}.lookup or {entity_name}.browse identifier, for example, sales$Customer.lookup. So make sure you have one when using components mentioned above. Studio registers browse screens with {entity_name}.browse identifiers, so they are automatically used as lookup screens.

5.5.1.1.4. Edit Screen

Edit screen is designed to display and edit entity instances. It initializes the instance being edited and contains actions for committing changes to the database. Edit screen should be opened by the openEditor() method accepting an entity instance as an argument.

By default, the standard CreateAction and EditAction open a screen, registered in screens.xml with the {entity_name}.edit identifier, for example, sales$Customer.edit.

Edit screen controller must be inherited from the AbstractEditor class.

Tip

You can create an edit screen for an entity in Studio using the Entity editor template.

The datasource attribute of a screen’s XML should refer to a datasource containing the edited entity instance. The following standard button frames in the XML can be used to display actions that commit or cancel changes:

  • editWindowActions (file com/haulmont/cuba/gui/edit-window.actions.xml) – contains OK and Cancel buttons

  • extendedEditWindowActions (file com/haulmont/cuba/gui/extended-edit-window.actions.xml) – contains OK & Close, OK and Cancel

The following actions are implicitly initialized in the edit screen:

  • windowCommitAndClose (corresponds to the Window.Editor.WINDOW_COMMIT_AND_CLOSE constant) – an action committing changes to the database and closing the screen. The action is initialized if the screen has a visual component with windowCommitAndClose identifier. The action is displayed as an OK & Close button when the mentioned above standard extendedEditWindowActions frame is used.

  • windowCommit (corresponds to the Window.Editor.WINDOW_COMMIT constant) – an action which commits changes to the database. In absence of windowCommitAndClose action, closes the screen after committing. The action is always displayed as an OK button if the screen has the abovementioned standard frames.

  • windowClose (corresponds to the Window.Editor.WINDOW_CLOSE constant) – which closes the screen without committing any changes. The action is always initialized. If the screen has the abovementioned standard frames, it is displayed as Cancel button.

Thus, if the screen contains an editWindowActions frame, the OK button commits the changes and closes the screen, and the Cancel button – closes the screen without committing the changes. If the screen contains an extendedEditWindowActions frame, the OK button only commits the changes, OK & Close button commits the changes and closes the screen, and the Cancel button closes the screen without committing the changes.

Instead of standard frames, the actions can be visualized using arbitrary components, for example, LinkButton.