A newer version is available at https://doc.cuba-platform.com/manual-latest.

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:

If you have any suggestions for improving this manual, feel free to report issues in the source repository on GitHub. If you see a spelling or wording mistake, a bug or inconsistency, don’t hesitate to fork the repo and fix it. Thank you!

1. Setup

System requirements
  • 64-bit operating system: Windows, Linux or macOS.

  • Memory – 8 GB for development with CUBA Studio.

  • Hard drive free space – 10 GB.

Java SE Development Kit (JDK)
  • Install JDK 8 and check it by running the following command in the console:

    java -version

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

    CUBA 7.1 supports Java 8, 9, 10 and 11. If you don’t need to work with projects based on previous CUBA versions (including migration to CUBA 7.1), then we recommend using Java 11.

  • Set the path to the JDK root directory in the JAVA_HOME environment variable, e.g. C:\Java\jdk8u202-b08.

    • On Windows, you can do this at ComputerPropertiesAdvanced System SettingsAdvancedEnvironment variables. The value of the variable should be added to the System variables list.

    • On macOS, it is recommended to install JDK in the /Library/Java/JavaVirtualMachines folder, for example /Library/Java/JavaVirtualMachines/jdk8u202-b08, and set JAVA_HOME in ~/.bash_profile with the following command:

      export JAVA_HOME="$(/usr/libexec/java_home -v 1.8)"

  • If you connect to the internet via a proxy server, some Java system properties must be passed to the JVM running development tools 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 the properties system-wide in the JAVA_OPTS environment variable.

Development Tools

The following tools facilitate development with the CUBA framework:

  • CUBA Studio - an integrated development environment built on the IntelliJ platform and tailored specifically for CUBA projects. You can install it as a separate application for your operating system, or as a plugin to IntelliJ IDEA (Community or Ultimate). See more information in the CUBA Studio User Guide.

  • CUBA CLI - a command line tool that provides basic scaffolding of projects and their elements: entities, screens, services, etc. This tool allows you to use any Java IDE for development of CUBA applications. See more information on the CUBA CLI GitHub page.

If you are new to Java, we strongly recommend using CUBA Studio as it is the most advanced and intuitive tool.

Database

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.

Web browser

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

2. Quick Start

This section describes the process of creating an application using CUBA Studio.

Make sure that the necessary software is already installed and set up on your computer, see 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.

2.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.

2.2. Creating a Project

  1. Start CUBA Studio.

  2. Click Create New Project.

    start studio
  3. Make sure that Java SE Development Kit (JDK) 8 is installed and selected as the project JDK by default.

    The list of repositories already contains binary artifacts repository URL and authentication parameters.

    create project
  4. 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.

    The following fields below will be automatically populated:

    • Project location – 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 on your hard drive. You can select one of those, or create a new directory.

    • 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.

    new project
  5. Click Finish. The empty project will be created in the specified sales directory, and the main Studio workspace will open.

    If it is your first use of Studio, it will start with downloading and connecting to Gradle daemon. Also, during the first use of the particular platform build, Studio will download the sources and binary artifacts of the platform. In this case opening the project and assembling the application may take some time. Before starting to work on the project, wait until the synchronization and indexation finish.

    studio workspace
  6. Create the database on the local HSQL database server: select option CUBA > Create database in the main menu. The database name is the same as project namespace by default.

  7. Select CUBA > Start application server option. Also, you can use Run Configuration dropdown on the toolbar to run the application. The link in the Runs at…​ section of the CUBA project tree will help to open the application in a web browser 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.

2.3. Creating Entities

Let’s create the Customer entity class.

  • In the Data Model section of the CUBA project tree, right-click on this node and select New > Entity. The New CUBA Entity dialog window will appear.

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

    new entity
  • Click OK. The entity designer page will be displayed in the workspace.

    entity designer
  • The entity name and the database table name will be automatically generated in the Entity name and the Table fields respectively.

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

  • Leave the Inheritance field unchanged.

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. Check the Mandatory box. The name of the database table column will be automatically generated in the Column field.

    new attribute

    Click Add to add the attribute.

  • email attribute is created in the same way. For this attribute we will add validation. After creating the attribute click on Email - not set link in the Validation section of the attribute property palette.

    email attribute
  • In the dialog check Enabled checkbox and enter validation error message Email address is not valid and click OK.

    email validation dialog

Now switch to the Text tab. It contains the source code of the Customer class.

customer code

Customer entity creation is now complete.

Let’s create the Order entity.

Right-click on the the Data Model node of the CUBA project tree, click New > Entity. Enter the Entity 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.

new entity order

2.4. Creating Database Tables

It is sufficient to click CUBA > Generate Database Scripts in the main menu to create the database tables. After that, Database Scripts page will open.

The incremental DB update scripts from its current state are displayed on the Updates tab:

db scripts

The generated scripts for initial DB creation are available on Init Tables, Init Constraints, and Init Data tabs.

db scripts init

Click Save and close button to save the generated scripts.

To run update scripts, select CUBA > Update database. You might need to stop the application server in order to do this.

2.5. Creating User Interface Screens

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

2.5.1. Screens for Customer

Right-click on Customer entity in the Data Model section of the CUBA project tree, and in its context menu select New > Screen to create standard screens for viewing and editing Customer instances. After that, the template browser page will appear.

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

screen templates

All fields in this dialog are already populated with default values, there is no need to change them. Click Next, leave localizable messages unchanged on the next screen and click Finish .

customer screens

The screen files will appear in the Screens section of the Generic UI tree section:

  • customer-browse.xml - browser screen descriptor,

  • CustomerBrowse - browser screen controller,

  • customer-edit.xml - editor screen descriptor,

  • CustomerEdit - editor screen controller.

2.5.2. Order Screens

The 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 section of CUBA project tree, right-click on the Order entity and in its context menu select New > View. View designer page will open. Enter order-with-customer as the view name, click on customer attribute and select the _minimal view for the Customer entity on the panel on the right.

new view

Click OK.

After that, select the Order entity and in its context menu select New > Screen.

Select Entity browser and editor screens template.

Select order-with-customer as the view for both browser and editor screens and click Next and then Finish on the next screen.

order screens

The screen files will appear in the Screens section of the Generic UI tree section:

  • order-browse.xml - browser screen descriptor,

  • OrderBrowse - browser screen controller,

  • order-edit.xml - editor screen descriptor,

  • OrderEdit - editor screen controller.

2.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 section of CUBA project tree and open Web Menu. The web-menu.xml descriptor will open.

Change the menu identifier from application-sales to shop either from code or in the visual designer on the Structure tab.

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<menu-config xmlns="http://schemas.haulmont.com/cuba/menu.xsd">
    <menu id="shop" insertBefore="administration">
        <item screen="sales_Customer.browse"/>
        <item screen="sales_Order.browse"/>
    </menu>
</menu-config>

Then, open the messages.properties file in the Main Message Pack section and modify the old caption of the menu item:

menu-config.shop = Shop

2.5.4. Customer Editor With a List of Orders

Do the following to display the list of orders in the Customer edit screen:

Go to the Generic UI section of CUBA project tree. Open the customer-edit.xml screen for editing.

Switch to the Designer tab.

In the components palette find Collection in the Data components group. Drag this component to the data section in screen components hierarchy panel.

Select the com.company.sales.entity.Order entity and its _local view for the data container. Generate the loader ID using the generate_id button.

Add the WHERE clause to the generated query to select only the orders that have a reference to the edited customer:

select e from sales_Order e where e.customer = :customer

Finally, you should see a data container which will load the Order instances in customer-edit screen XML descriptor:

<data>
    <instance id="customerDc" class="com.company.sales.entity.Customer" view="_local">
        <loader/>
    </instance>
    <collection id="ordersDc" class="com.company.sales.entity.Order" view="_local">
        <loader id="ordersDl">
            <query><![CDATA[select e from sales_Order e where e.customer = :customer]]></query>
        </loader>
    </collection>
</data>

Then, drag Label from the components palette to components hierarchy panel and place it between form and editActions. Go to the Properties tab on the properties panel. Enter Orders in the value field.

If the application is intended to be used in multiple languages, use the localization button next to the caption 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 editActions. Select this component in the hierarchy and specify table size in the Properties tab: set 300px in the width field and 200px in the height field. Choose ordersDc from the list of available data containers. Then generate the table identifier using the generate_id button next to the id field: ordersTable.

Next, right-click on the table and select Wrap Into > Group Box from the context menu. Go to the Properties tab on the properties panel of this group box and enter Orders in the caption field and set group box width to 320 px.

customer edit

As a result, the customer-edit.xml code on the Text tab will look as follows:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd" caption="msg://editorCaption" focusComponent="form"
        messagesPack="com.company.sales.web.customer">
    <data>
        <instance id="customerDc" class="com.company.sales.entity.Customer" view="_local">
            <loader/>
        </instance>
        <collection id="ordersDc" class="com.company.sales.entity.Order" view="_local">
            <loader id="ordersDl">
                <query><![CDATA[select e from sales_Order e where e.customer = :customer]]></query>
            </loader>
        </collection>
    </data>
    <dialogMode height="600" width="800"/>
    <layout expand="editActions" spacing="true">
        <form id="form" dataContainer="customerDc">
            <column width="250px">
                <textField id="nameField" property="name"/>
                <textField id="emailField" property="email"/>
            </column>
        </form>
        <groupBox caption="Orders">
            <table id="ordersDcTable" dataContainer="ordersDc" height="200px" width="300px">
                <columns>
                    <column id="date"/>
                    <column id="amount"/>
                </columns>
            </table>
        </groupBox>
        <hbox id="editActions" spacing="true">
            <button action="windowCommitAndClose"/>
            <button action="windowClose"/>
        </hbox>
    </layout>
</window>

Open the CustomerEdit screen controller. The © and <> buttons on the gutter enable quick switching between the screen descriptor and controller.

First - we need to disable automatic data load for the screen, because we need custom data load process. To disable automatic data load, remove @LoadDataBeforeShow annotation from the class.

Inject the orders loader in the controller class by pressing Alt+Insert inside the class definition and selecting ordersDl from the list. Or you can just type the following code manually:

@Inject
private CollectionLoader<Order> ordersDl;

Then, subscribe to the BeforeShowEvent to set the customer parameter value for the ordersDl data loader. To do this, click Alt+Insert znd select Subscribe to Event > BeforeShowEvent in the Generate menu. And, of course, you can just add the following code manually.

@UiController("sales_Customer.edit")
@UiDescriptor("customer-edit.xml")
@EditedEntityContainer("customerDc")
public class CustomerEdit extends StandardEditor<Customer> {
    @Inject
    private CollectionLoader<Order> ordersDl;

    @Subscribe
    protected void onBeforeShow(BeforeShowEvent event) {
        ordersDl.setParameter("customer", getEditedEntity());
        getScreenData().loadAll();
    }
}

This method will be in charge of loading related Order instances.

2.6. Running the Application

Now let’s see how the created screens look in the actual application. Select CUBA > Start application server or just click on the IDEA’s "Run" button on the toolbar.

Log in using default credentials in the login window. Open the Shop > Customers menu item:

customer browse
Figure 1. The Customers browser

Click Create and create a new customer:

customer edit 2
Figure 2. The Customer editor screen

Open the Shop > Orders menu item:

orders browse
Figure 3. The Orders browser

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

order edit
Figure 4. The Order editor

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

customer edit 3
Figure 5. The Customer editor

3. The Framework

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

3.1. Architecture

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

3.1.1. Application Tiers and Blocks

The framework enables building multi-tiered applications with the distinct client, middle and database tiers. Further on, we will consider mainly the middle and client tiers, so the words "all tiers" will refer to these tiers only.

An application can have one or more blocks on each tier. A block is a separate executable program interacting with other blocks of the application. Usually, a block is a web application running on JVM.

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 a Java servlet container. See Middleware Components.

Web Client

A main block of the client tier. It contains the interface designed primarily for internal (back-office) users. It is represented by a separate web application running on a Java servlet container. The user interface is based on the Vaadin 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 a Java servlet container. The user interface is based on the Spring MVC framework. See Portal Components.

Frontend UI

An optional UI layer designed for external users and written in pure JavaScript. It is based on the Google Polymer or React frameworks and communicates with the middleware via REST API running either in Web Client or in Web Portal blocks. See Frontend 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 Web Portal.

All of the Java-based client blocks interact with the middle tier uniformly via HTTP protocol which enables deploying the middle tier arbitrarily, behind a 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.

3.1.2. Application Modules

A module is the smallest structural part of a CUBA application. It is a single module of an 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.

  • gui – contains components of the generic user interface. It is used in the Web Client block.

  • web – the implementation of the generic user interface based on Vaadin.

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

  • front – an optional module – implementation of Frontend User Interface in JavaScript.

AppModules
Figure 7. Application Modules

3.1.3. Application Components

The framework enables splitting the application functionality into components. Each application component (AKA add-on) can have its own data model, business logic and user interface. The application uses the component as a library and includes its functionality.

The concept of application components allows us to keep the framework relatively small, while delivering optional business functionality in the components like Reporting, Full-Text Search, Charts, WebDAV and others. At the same time, the application developers can use this mechanism to decompose large projects into a set of functional modules which can be developed independently and have a different release cycle. Of course, application components can be reusable and provide a domain-specific layer of abstraction on top of the framework.

Technically, the core framework is also an application component called cuba. The only difference is that it is mandatory for any application. All other components depend on cuba and can also have dependencies between each other.

Below is a diagram showing dependencies between the standard components typically used in an application. Solid lines demonstrate mandatory dependencies, dashed lines mean optional ones.

BaseProjects

The following diagram illustrates a possible structure of dependencies between standard and custom application components.

AppComponents2

Any CUBA application can be easily turned into a component and provide some functionality to another application. 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 the step-by-step guide to working with a custom application component in the Example of Application Component section.

3.1.4. Application Structure

The above-listed architectural principles are directly reflected in the structure of the assembled application. Suppose we have a simple application which has two blocks – Middleware and Web Client; and includes the functionality of 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 the deployed application.

The Middleware block is represented by the app-core web application, the Web Client block – by the app web application. The web applications contain the JAR files located in the WEB-INF/lib directories. Each JAR (artifact) is a result of building a single module of an application or one of its components.

For instance, the set of JAR files of the app-core web application is determined by the fact that the Middleware block includes global and core modules; and the application uses cuba and reports components.

3.2. Common Components

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

3.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.time.LocalDate

  • java.time.LocalTime

  • java.time.LocalDateTime

  • java.time.OffsetTime

  • java.time.OffsetDateTime

  • 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.

3.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.

    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 and data components 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.

  • BaseGenericIdEntity – base class of persistent and non-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. Non-persistent entities should be inherited from the same base classes as persistent ones. The framework determines if the entity is persistent or not by the file where it is registered: persistence.xml or metadata.xml.

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
BaseIntIdentityIdEntity

Inherit from BaseIntIdentityIdEntity if you need to map the entity to a table with IDENTITY primary key of Integer type (compared to Long in BaseIdentityIdEntity). In other respects, BaseIntIdentityIdEntity is similar to BaseIdentityIdEntity.

EntityClasses IntIdentity
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.

3.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.

3.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")
@IdSequence

Explicitly defines the name of a database sequence that should be used for generating identifiers if the entity is a subclass of BaseLongIdEntity or BaseIntegerIdEntity. If the entity is not annotated, the framework creates a sequence with an automatically generated name.

Parameters:

  • name – sequence name.

  • cached - optional parameter which defines that the sequence should be incremented by cuba.numberIdCacheSize to cache intermediate values in memory. False by default.

@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

Defines how to create a string which represents a single instance of the entity. Think of it as of an application level toString() method. It is used extensively in UI when displaying an entity instance in a single field like TextField or LookupField. You can also get the instance name programmatically using the MetadataTools.getInstanceName() method.

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

  • {0} – format string which can be one of two types:

    • A string with %s placeholders for formatted values of entity attributes. Attribute values are formatted to strings according to their datatypes.

    • A name of this object’s method with the # prefix. The method should return String and should have no parameters.

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

Examples:

@NamePattern("%s|name")
@NamePattern("%s - %s|name,date")
@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")
@PublishEntityChangedEvents

Indicates that EntityChangedEvent should be sent by the framework when the entity is changed in the database.

@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. The screen can be added to the main menu using the following element of web-menu.xml:

<item id="sec$ScreenHistory.browse" insertAfter="settings"/>
3.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 collection container 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 can have an owning side and an 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.

The usage of cascade annotation attribute is not recommended. The entities persisted and merged implicitly using such declaration will bypass some system mechanisms. In particular, the EntityStates bean does not detect the managed state correctly and entity listeners are not invoked at all.

@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;

The usage of JPA cascade annotation attribute is not recommended. The entities persisted and merged implicitly using such declaration will bypass some system mechanisms. In particular, the EntityStates bean does not detect the managed state correctly and entity listeners are not invoked at all.

@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;
    }
}
@NumberFormat

Specifies a format for an attribute of the Number type (it can be BigDecimal, Integer, Long or Double). Values of such attribute will be formatted and parsed throughout the UI according to the provided annotation parameters:

  • pattern - format pattern as described for DecimalFormat.

  • decimalSeparator - character used as a decimal sign (optional).

  • groupingSeparator - character used as a thousands separator (optional).

If decimalSeparator and/or groupingSeparator are not specified, the framework uses corresponding values from the format strings for the current user’s locale. The server system locale characters are used in this case for formatting the attribute values with locale-independent methods.

For example:

@Column(name = "PRECISE_NUMBER", precision = 19, scale = 4)
@NumberFormat(pattern = "0.0000")
protected BigDecimal preciseNumber;

@Column(name = "WEIRD_NUMBER", precision = 19, scale = 4)
@NumberFormat(pattern = "#,##0.0000", decimalSeparator = "_", groupingSeparator = "`")
protected BigDecimal weirdNumber;

@Column(name = "SIMPLE_NUMBER")
@NumberFormat(pattern = "#")
protected Integer simpleNumber;

@Column(name = "PERCENT_NUMBER", precision = 19, scale = 4)
@NumberFormat(pattern = "#%")
protected BigDecimal percentNumber;
@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;

The usage of JPA cascade and orphanRemoval annotation attributes is not recommended. The entities persisted and merged implicitly using such declaration will bypass some system mechanisms. In particular, the EntityStates bean does not detect the managed state correctly and entity listeners are not invoked at all. The orphanRemoval annotation attribute does not respect the soft deletion mechanism.

@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;
3.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 using Data Model > New > Enumeration menu. 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.

3.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.

3.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 data loaders level – calling DataLoader.setSoftDeletion(false) or setting softDeletion="false" attribute of loader 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.

3.2.1.4.2. Related Entities Processing Policy

For soft deleted entities, 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 implementing SoftDelete 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.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)

3.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.

3.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.

3.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).

3.2.2.3. Datatype

Datatype interface defines methods for converting values to and from strings (formatting and parsing). Each entity attribute, if it is not a reference, has a corresponding Datatype, which is used by the framework to format and parse the attribute value.

Datatypes are registered in the DatatypeRegistry bean, which loads and initializes Datatype implementation classes from the metadata.xml files of the project and its application components.

Datatype of an entity attribute can be obtained from the corresponding meta-property using getRange().asDatatype() call.

You can also use registered datatypes to format or parse arbitrary values of supported types. To do this, obtain a datatype instance from DatatypeRegistry using its get(Class) or getNN(Class) methods, passing the Java type that you want to convert.

Datatypes are associated with entity attributes according to the following rules:

  • In most cases, an attribute is associated with a registered Datatype instance that can handle the attribute’s Java type.

    In the example below, the amount attribute will get BigDecimalDatatype

    @Column(name = "AMOUNT")
    private BigDecimal amount;

    because com/haulmont/cuba/metadata.xml has the following entry:

    <datatype id="decimal" class="com.haulmont.chile.core.datatypes.impl.BigDecimalDatatype"
              default="true"
              format="0.####" decimalSeparator="." groupingSeparator=""/>
  • You can specify a datatype explicitly using the @MetaProperty annotation and its datatype attribute.

    In the example below, the issueYear entity attribute will be associated with the year datatype:

    @MetaProperty(datatype = "year")
    @Column(name = "ISSUE_YEAR")
    private Integer issueYear;

    if the project’s metadata.xml file has the following entry:

    <datatype id="year" class="com.company.sample.YearDatatype"/>

    As you can see, the datatype attribute of @MetaProperty contains identifier, which is used when registering the datatype implementation in metadata.xml.

Basic methods of the Datatype interface:

  • format() – converts the passed value into a string.

  • parse() – transforms a string into the value of corresponding type.

  • getJavaClass() – returns the Java type which this datatype is designed for. This method has a default implementation that returns a value of the @JavaClass annotation if it is present on the class.

Datatype defines 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 hardcoded or specified in the metadata.xml file when registering the datatype.

See the next section for how to specify locale-dependent parsing formats.

3.2.2.3.1. Datatype Format Strings

Locale-dependent parsing formats are provided in the main messages pack of the application or its components, in the strings with the following keys:

  • numberDecimalSeparator – decimal separator for numeric types.

  • numberGroupingSeparator – thousands separator for numeric types.

  • 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.

Studio allows you to set format strings for languages used in your application. Edit Project Properties, click the button in the Available locales field, then click Show data format strings.

Format strings for a locale can be obtained using the FormatStringsRegistry bean.

3.2.2.3.2. Example of a Custom Datatype

Suppose that some entity attributes in our application store calendar years, represented by integer numbers. Users should be able to view and edit a year, and if a user enters just two digits, the application should transform it to a year between 2000 and 2100. Otherwise, the whole entered number should be accepted as a year.

First, create the following class in the global module:

package com.company.sample.entity;

import com.google.common.base.Strings;
import com.haulmont.chile.core.annotations.JavaClass;
import com.haulmont.chile.core.datatypes.Datatype;

import javax.annotation.Nullable;
import java.text.DecimalFormat;
import java.text.ParseException;
import java.util.Locale;

@JavaClass(Integer.class)
public class YearDatatype implements Datatype<Integer> {

    private static final String PATTERN = "##00";

    @Override
    public String format(@Nullable Object value) {
        if (value == null)
            return "";

        DecimalFormat format = new DecimalFormat(PATTERN);
        return format.format(value);
    }

    @Override
    public String format(@Nullable Object value, Locale locale) {
        return format(value);
    }

    @Nullable
    @Override
    public Integer parse(@Nullable String value) throws ParseException {
        if (Strings.isNullOrEmpty(value))
            return null;

        DecimalFormat format = new DecimalFormat(PATTERN);
        int year = format.parse(value).intValue();
        if (year > 2100 || year < 0)
            throw new ParseException("Invalid year", 0);
        if (year < 100)
            year += 2000;
        return year;
    }

    @Nullable
    @Override
    public Integer parse(@Nullable String value, Locale locale) throws ParseException {
        return parse(value);
    }
}

Then add the datatypes element to the metadata.xml of your project:

<metadata xmlns="http://schemas.haulmont.com/cuba/metadata.xsd">
    <datatypes>
        <datatype id="year" class="com.company.sample.entity.YearDatatype"/>
    </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[]

In our case the class is designed to work with Integer type (which is declared by the @JavaClass annotation with Integer.class value), so the sqlType attribute can be omitted.

Finally, specify the new datatype for the required attributes (programmatically or with the help of Studio):

@MetaProperty(datatype = "year")
@Column(name = "ISSUE_YEAR")
private Integer issueYear;
3.2.2.3.3. Example of Data Formatting in UI

Let’s consider how the Order.date attribute is displayed in orders table.

order-browse.xml

<table id="ordersTable">
    <columns>
        <column id="date"/>
        <!--...-->

The date attribute in the 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:

dateFormat=dd.MM.yyyy

As a result, date "2012-08-06" is converted into the string "06.08.2012" which is displayed in the table cell.

3.2.2.3.4. 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;

void sample() {
    String dateStr = formatter.formatDate(dateField.getValue());
    // ...
}

Below are examples of using Datatype methods directly.

  • Date formatting example:

    @Inject
    protected UserSessionSource userSessionSource;
    @Inject
    protected DatatypeRegistry datatypes;
    
    void sample() {
        Date date;
        // ...
        String dateStr = datatypes.getNN(Date.class).format(date, userSessionSource.getLocale());
        // ...
    }
  • Example of formatting numeric values with up to 5 decimal places in Web Client:

    com/sample/sales/web/messages_ru.properties
    coordinateFormat = #,##0.00000
    @Inject
    protected Messages messages;
    @Inject
    protected UserSessionSource userSessionSource;
    @Inject
    protected FormatStringsRegistry formatStringsRegistry;
    
    void sample() {
        String coordinateFormat = messages.getMainMessage("coordinateFormat");
        FormatStrings formatStrings = formatStringsRegistry.getFormatStrings(userSessionSource.getLocale());
        NumberFormat format = new DecimalFormat(coordinateFormat, formatStrings.getFormatSymbols());
        String formattedValue = format.format(value);
        // ...
    }
3.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>

3.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.

  • loadPartialEntities - specifies whether the view affects loading of local (in other words, immediate) attributes. If false, only reference attributes are affected, and local ones are loaded regardless of their presence in the view.

    This property is controlled to some extent by the platform data loading mechanisms, see the sections about loading partial entities in DataManager and EntityManager.

ViewProperty class has the following properties:

  • name – the name of the entity attribute.

  • view – for reference attributes, specifies the view 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.

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.

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 EntityStates.isLoaded() method.

3.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.

Three views named _local, _minimal and _base 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.

  • _base includes all local non-system attributes and attributes defined by @NamePattern (effectively _minimal + _local).

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="order-with-customer"
      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.

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.

3.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.

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.

3.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.

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.

3.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);
        return 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.

3.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.

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.

3.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.*;
    import com.haulmont.cuba.core.sys.jmx.JmxBean;
    
    @JmxBean(module = "sales", alias = "OrdersMBean")
    @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 @JmxBean annotation is used for automatic registration of the class instances with a JMX server, according to the module and alias attributes. You can register JMX bean using this annotation instead of registration in a spring.xml.

    • 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.

      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.

  • If you did not set the @JmxBean annotation for the JMX bean interface, you should register the JMX bean in the 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).

3.2.5.2. The Platform JMX Beans

This section describes some of the JMX beans available in the platform.

3.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

3.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

3.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

3.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

3.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()
      }
3.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

3.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.).

3.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();
3.2.6.2. 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.

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(Class) - loads entities of the specified class. This method is an entry point to the fluent API:

    @Inject
    private DataManager dataManager;
    
    private Book loadBookById(UUID bookId) {
        return dataManager.load(Book.class).id(bookId).view("book.edit").one();
    }
    
    private List<BookPublication> loadBookPublications(UUID bookId) {
        return dataManager.load(BookPublication.class)
            .query("select p from library_BookPublication p where p.book.id = :bookId")
            .parameter("bookId", bookId)
            .view("bookPublication.full")
            .list();
    }
  • loadValues(String query) - loads key-value pairs by the query for scalar values. This method is an entry point to the fluent API:

    List<KeyValueEntity> list = dataManager.loadValues(
            "select o.customer, sum(o.amount) from demo_Order o " +
            "where o.date >= :date group by o.customer")
        .store("legacy_db") (1)
        .properties("customer", "sum") (2)
        .parameter("date", orderDate)
        .list();
    1 - specify data store where the entity is located. Omit this method if the entity is located in the main data store.
    2 - specify names of the resulting KeyValueEntity attributes. The order of the properties must correspond to the columns in the query result set.
  • loadValue(String query, Class valueType) - loads a single value by the query for scalar values. This method is an entry point to the fluent API:

    BigDecimal sum = dataManager.loadValue(
            "select sum(o.amount) from demo_Order o " +
            "where o.date >= :date group by o.customer", BigDecimal.class)
        .store("legacy_db") (1)
        .parameter("date", orderDate)
        .one();
    1 - specify data store where the entity is located. Omit this method if the entity is located in the main data store.
  • load(LoadContext), loadList(LoadContext) – load 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.

    For example:

    @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(ValueLoadContext) - 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(LoadContext) - 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(CommitContext) – 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.

    DataManager can perform bean validation of saved entities.

    Examples of 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(Entity, View) - convenience method to reload a specified instance from the database with the required view. It delegates to the load() method.

  • remove(Entity) - removes a specified instance from the database. Delegates to commit() method.

  • create(Class) - creates an instance of the given entity in memory. This is a convenience method that just delegates to Metadata.create().

  • getReference(Class, Object) - returns an entity instance which can be used as a reference to an object which exists in the database.

    For example, if you are creating a User, you have to set a Group the user belongs to. If you know the group id, you could load it from the database and set to the user. This method saves you from unneeded database round trip:

    user.setGroup(dataManager.getReference(Group.class, groupId));
    dataManager.commit(user);

    A reference can also be used to delete an existing object by id:

    dataManager.remove(dataManager.getReference(Customer.class, customerId));
Query

When working with relational databases, use JPQL queries to load data. See the JPQL Functions, Case-Insensitive Substring Search and Macros in JPQL sections for information on how JPQL in CUBA differs from the JPA standard. Also note that DataManager can execute only "select" queries and use only named parameters; positional parameters are not supported.

Transactions

DataManager always starts a new transaction and commits it on operation completion, thus returning entities in the detached state. On the middle tier, you can use TransactionalDataManager if you need to implement complex transactional behavior.

Partial entities

Partial entity is an entity instance that can have only a subset of local attributes loaded. By default, DataManager loads partial entities according to views (in fact, RdbmsStore just sets the loadPartialEntities property of the view to true and passes it down to EntityManager).

There are some conditions, when DataManager loads all local attributes and uses views only for fetching references:

  • The loaded entity is cached.

  • In-memory "read" constraints are defined for the entity.

  • Dynamic attribute access control is set up for the entity.

  • The loadPartialEntities attribute of LoadContext is set to false.

3.2.6.2.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 can perform bean validation when saving entities.

EntityManager doesn’t perform bean validation.

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. On the middle tier, you can use TransactionalDataManager if you need to implement complex transactional behavior.

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 TransactionalDataManager when you need to implement some atomic logic inside a transaction or EntityManager if it is better suited to the task. In general, on the middleware you can use any of these interfaces.

If you need to overcome restrictions of DataManager when working on the client tier, create your own service and use TransactionalDataManager or 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.

3.2.6.2.2. TransactionalDataManager

TransactionalDataManager is a bean of the middle tier which mimics the DataManager interface but can join an existing transaction. It has the following features:

  • If there is an active transaction, joins it, otherwise creates and commits a transaction same as DataManager.

  • It accepts and returns entities in detached state. The developer should load entities with appropriate views and explicitly use the save() method to save modified instances to the database.

  • It applies row-level security, works with dynamic attributes and cross-datastore references in the same way as DataManager.

Below is a simple example of using TransactionalDataManager in a service method:

@Inject
private TransactionalDataManager txDataManager;

@Transactional
public void transfer(Id<Account, UUID> acc1Id, Id<Account, UUID> acc2Id, Long amount) {
    Account acc1 = txDataManager.load(acc1Id).one();
    Account acc2 = txDataManager.load(acc2Id).one();
    acc1.setBalance(acc1.getBalance() - amount);
    acc2.setBalance(acc2.getBalance() + amount);
    txDataManager.save(acc1);
    txDataManager.save(acc2);
}

You can find more complex example in the framework test: DataManagerTransactionalUsageTest.java

TransactionalDataManager is especially useful when handling EntityChangedEvent in the current transaction. It allows you to get the current state of the changed entity from the database before the transaction is committed.

3.2.6.2.3. 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 on the middleware only if you additionally set the cuba.entityAttributePermissionChecking application property to true. It makes sense if Middleware serves remote clients that theoretically can be hacked, like a desktop client. In this case, set also the cuba.keyForSecurityTokenEncryption application property to a unique value. If your application uses only Web or Portal clients, you can safely keep default values of these properties.

Note that access group constraints (row-level security) are always applied regardless of the above conditions.

See also the Data Access Checks section for the whole picture of how security permissions and constraints are used by different mechanisms of the framework.

3.2.6.2.4. 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.

3.2.6.2.5. 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 is periodically cleaned of old query results left by terminated user sessions. This is done by the deleteForInactiveSessions() method of the QueryResultsManagerAPI bean which is invoked by a Spring scheduler defined in cuba-spring.xml. By default, it is done once in 10 minutes, but you can set a desired interval in milliseconds using the cuba.deleteOldQueryResultsInterval application property of the core module.

3.2.6.3. EntityStates

An interface for obtaining the information on persistent entities managed by ORM. Unlike the Persistence and PersistenceTools beans, this interface is available on all tiers.

The EntityStates interface has the following methods:

  • 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.

  • 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 can be checked by this method.

  • checkLoaded() - the same as isLoaded() but throws IllegalArgumentException if at least one of the attributes passed to the method is not loaded.

  • isLoadedWithView() - accepts an entity instance and a view and returns true if all attributes required by the view are actually loaded.

  • checkLoadedWithView() - the same as isLoadedWithView() but throws IllegalArgumentException instead of returning false.

  • makeDetached() - accepts a newly created entity instance and turns it into the detached state. The detached object can be passed to DataManager.commit() or EntityManager.merge() to save its state in the database. See details in the API docs.

  • makePatch() - accepts a newly created entity instance and makes it a patch object. The patch object can be passed to DataManager.commit() or EntityManager.merge() to save its state in the database. Unlike for a detached object, only non-null attributes will be saved. See details in the API docs.

3.2.6.3.1. PersistenceHelper

A helper class with static methods delegating to the EntityStates interface.

3.2.6.4. Events

Events bean encapsulates the application-scope event publication functionality. Application events can be used to exchange information between loosely coupled components. Events bean is a simple facade for ApplicationEventPublisher of the Spring Framework.

public interface Events {
    String NAME = "cuba_Events";

    void publish(ApplicationEvent event);
}

It has only one method publish() that receives an event object. Events.publish() notifies all matching listeners registered with this application of an application event. You can use PayloadApplicationEvent to publish any object as an event.

Event handling in beans

First of all, we have to create a new event class. It should extend the ApplicationEvent class. An event class can contain any additional data. For instance:

package com.company.sales.core;

import com.haulmont.cuba.security.entity.User;
import org.springframework.context.ApplicationEvent;

public class DemoEvent extends ApplicationEvent {

    private User user;

    public DemoEvent(Object source, User user) {
        super(source);
        this.user = user;
    }

    public User getUser() {
        return user;
    }
}

Beans can publish an event using the Events bean:

package com.company.sales.core;

import com.haulmont.cuba.core.global.Events;
import com.haulmont.cuba.core.global.UserSessionSource;
import com.haulmont.cuba.security.global.UserSession;
import org.springframework.stereotype.Component;
import javax.inject.Inject;

@Component
public class DemoBean {

    @Inject
    private Events events;
    @Inject
    private UserSessionSource userSessionSource;

    public void demo() {
        UserSession userSession = userSessionSource.getUserSession();
        events.publish(new DemoEvent(this, userSession.getUser()));
    }
}

By default, all events are handled synchronously.

There are two ways to handle events:

  • Implement the ApplicationListener interface.

  • Use the @EventListener annotation for a method.

In the first case, we have to create a bean that implements ApplicationListener with the type of our event:

@Component
public class DemoEventListener implements ApplicationListener<DemoEvent> {
    @Inject
    private Logger log;

    @Override
    public void onApplicationEvent(DemoEvent event) {
        log.debug("Demo event is published");
    }
}

The second way can be used to hide implementation details and listen for multiple events in a single bean:

@Component
public class MultipleEventListener {
    @Order(10)
    @EventListener
    protected void handleDemoEvent(DemoEvent event) {
        // handle event
    }

    @Order(1010)
    @EventListener
    protected void handleUserLoginEvent(UserLoggedInEvent event) {
        // handle event
    }
}

By default, Spring events require protected, package or public access modifiers for @EventListener methods. Pay attention that private modifier is not supported.

Methods with @EventListener annotation do not work in services, JMX beans and other beans with interfaces. If you define @EventListener on such bean you will get the following error on application start:

BeanInitializationException: Failed to process @EventListener annotation on bean. Need to invoke method declared on target class, but not found in any interface(s) of the exposed proxy type. Either pull the method up to an interface or switch to CGLIB proxies by enforcing proxy-target-class mode in your configuration.

If you need to listen to an event in a bean with interface, implement the ApplicationListener interface instead.

You can use Spring Framework Ordered interface and @Order annotation for event handlers ordering. All the platform beans and event handlers use order value between 100 and 1000, thus you can add your custom handling before or after the platform code. If you want to add your bean or event handler before platform beans - use a value lower than 100.

See also Login Events.

Event handling in UI screens

Usually, Events delegates event publishing to the ApplicationContext. On the web tier, you can use a special interface for event classes - UiEvent. It is a marker interface for events that are sent to UIs screens in the current UI instance (the current web browser tab). Please note that UiEvent instances are not sent to Spring beans.

Sample event class:

package com.company.sales.web;

import com.haulmont.cuba.gui.events.UiEvent;
import com.haulmont.cuba.security.entity.User;
import org.springframework.context.ApplicationEvent;

public class UserRemovedEvent extends ApplicationEvent implements UiEvent {

    private User user;

    public UserRemovedEvent(Object source, User user) {
        super(source);
        this.user = user;
    }

    public User getUser() {
        return user;
    }
}

It can be fired using Events bean from a window controller the same way as from a bean:

@Inject
Events events;
// ...
UserRemovedEvent event = new UserRemovedEvent(this, removedUser);
events.publish(event);

In order to handle an event you have to define methods in UI screens with a special annotation @EventListener (ApplicationListener interface is not supported):

@Order(15)
@EventListener
protected void onUserRemove(UserRemovedEvent event) {
    notifications.create()
            .withCaption("User is removed " + event.getUser())
            .show();
}

You can use @Order annotation for event listener ordering.

If an event is UiEvent and fired using the Events bean from UI thread then opened windows and/or frames with such methods will receive the event. Event handling is synchronous. Only UI screens of the current web browser tab opened by the user receive the event.

3.2.6.5. 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).

3.2.6.5.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();
3.2.6.6. 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.

    For persistent BaseLongIdEntity and BaseIntegerIdEntity subclasses, assigns identifiers right after creation. The new identifiers are fetched from automatically created database sequences. By default, the sequences are created in the main data store. However, if the cuba.useEntityDataStoreForIdSequence application property is set to true, sequences are created in the data store the entity belongs to.

  • getTools() – returns MetadataTools interface instance (see below).

3.2.6.6.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();
3.2.6.7. 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.

3.2.6.8. 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.

3.2.6.9. 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.

3.2.6.10. 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();
3.2.6.11. UserSessionSource

The interface is used to obtain current user session object. See more in User Authentication.

3.2.6.12. 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.

3.2.7. 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

AppContext can be used in the application code for the following tasks:

  • 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.

  • 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");
        }
    });

    Please note that the recommended way to run code on the application startup and shutdown is using Application Lifecycle Events.

3.2.8. Application Lifecycle Events

There are the following types of lifecycle events in a CUBA application:

AppContextInitializedEvent

It is sent right after AppContext is initialized. At this moment:

  • 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 false.

  • The AppContext.isReady() method returns false.

AppContextStartedEvent

It is sent after AppContextInitializedEvent and after running all AppContext.Listener.applicationStarted(). At this moment:

  • The AppContext.isStarted() method returns true.

  • The AppContext.isReady() method returns false.

  • On the middleware, if cuba.automaticDatabaseUpdate application property is enabled, all database update scripts are successfully executed.

AppContextStoppedEvent

It is sent before the application shutdown and after running all AppContext.Listener.applicationStopped(). At this moment:

  • All the beans are operational and can be obtained via AppBeans.get() methods.

  • AppContext.isStarted() method returns false.

  • The AppContext.isReady() method returns false.

You can affect the order of listeners invocation by specifying the @Order annotation. The Events.HIGHEST_PLATFORM_PRECEDENCE and Events.LOWEST_PLATFORM_PRECEDENCE constants define the range which is used by listeners defined in the platform.

For example:

package com.company.demo.core;

import com.haulmont.cuba.core.global.Events;
import com.haulmont.cuba.core.sys.events.*;
import org.slf4j.Logger;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

import javax.inject.Inject;

@Component
public class MyAppLifecycleBean {

    @Inject
    private Logger log;

    // event type is defined by annotation parameter
    @EventListener(AppContextInitializedEvent.class)
    // run after all platform listeners
    @Order(Events.LOWEST_PLATFORM_PRECEDENCE + 100)
    protected void appInitialized() {
        log.info("Initialized");
    }

    // event type is defined by method parameter
    @EventListener
    protected void appStarted(AppContextStartedEvent event) {
        log.info("Started");
    }

    @EventListener
    protected void appStopped(AppContextStoppedEvent event) {
        log.info("Stopped");
    }
}
ServletContextInitializedEvent

It is published right after initialization of Servlet and Application contexts. At this moment:

  • Static AppBeans.get() methods can be used for obtaining beans.

  • This event contains application and servlet contexts, thus enabling to register custom Servlets, Filters and Listeners, see Registration of Servlets and Filters.

ServletContextDestroyedEvent

It is published when Servlet and Application are about to be shut down and enables to free resources manually.

For example:

@Component
public class MyInitializerBean {

    @Inject
    private Logger log;

    @EventListener
    public void foo(ServletContextInitializedEvent e) {
        log.info("Application and servlet context is initialized");
    }

    @EventListener
    public void bar(ServletContextDestroyedEvent e) {
        log.info("Application is about to shut down, all contexts are now destroyed");
    }
}

3.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.

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.

An additive property value obtained at runtime is formed by elements concatenated with a space symbol.

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.

3.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 in the appPropertiesConfig parameter of web.xml.

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.base 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.

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 .

3.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.

3.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();

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().

3.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.

  • If the @Source value is SourceType.DATABASE, the property can be edited on the Administration > Application Properties screen provided by the platform. You can use the @Secret annotation in order to mask the value on this screen (PasswordField will be used instead of the regular text field).

Config interfaces must be defined inside of the root package of the application (or in inner packages of the root package).

Example:

@Source(type = SourceType.DATABASE)
public interface SalesConfig extends Config {

    @Property("sales.companyName")
    String getCompanyName();

    @Property("sales.ftpPassword")
    @Secret
    String getFtpPassword();
}

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.

3.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
3.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();

3.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.

3.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
/com/abc/sales/gui/customer/messages_en_US.properties

This pack consists of 4 files – one for Russian, one for French, one for American English (with US country code), 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.

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.

3.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
3.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"
3.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.

3.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.

3.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 AuthenticationManager.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 AuthenticationManager.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.

3.2.11.2. Login

CUBA Platform provides built-in extensible authentication mechanisms. They include different authentication schemes such as login/password, remember me, trusted and anonymous login.

This section primarily describes authentication mechanisms of the middle tier. For web client specifics, see Web Login.

The platform includes the following authentication mechanisms on middleware:

  • AuthenticationManager implemented by AuthenticationManagerBean

  • AuthenticationProvider implementations

  • AuthenticationService implemented by AuthenticationServiceBean

  • UserSessionLog - see user session logging.

MiddlewareAuthenticationStructure
Figure 12. Authentication mechanisms of middleware

Also, it employs the following additional components:

  • TrustedClientService implemented by TrustedClientServiceBean - provides anonymous/system sessions to trusted clients.

  • AnonymousSessionHolder - creates and holds anonymous session instance for trusted clients.

  • UserCredentialsChecker - checks if user credentials can be used, for instance, protect against brute-force attack.

  • UserAccessChecker - checks if user can access system from the given context, for instance, from REST or using provided IP address.

The main interface for authentication is AuthenticationManager which contains four methods:

public interface AuthenticationManager {

    AuthenticationDetails authenticate(Credentials credentials) throws LoginException;

    AuthenticationDetails login(Credentials credentials) throws LoginException;

    UserSession substituteUser(User substitutedUser);

    void logout();
}

There are two methods with similar responsibility: authenticate() and login(). Both methods check if provided credentials are valid and corresponds to a valid user, then return AuthenticationDetails object. The main difference between them is that login method additionally activates user session, thus it can be used for calling service methods later.

Credentials represent a set of credentials for authentication subsystem. The platform has several types of credentials that are supported by AuthenticationManager:

Available for all tiers:

  • LoginPasswordCredentials

  • RememberMeCredentials

  • TrustedClientCredentials

Available only on middle tier:

  • SystemUserCredentials

  • AnonymousUserCredentials

AuthenticationManager login / authenticate methods return AuthenticationDetails instance which contains UserSession object. This object can be used to check additional permissions, read User properties and session attributes. There is only one built-in implementation of AuthenticationDetails interface - SimpleAuthenticationDetails that stores only user session object, but application can provide its own AuthenticationDetails implementation with additional information for clients.

AuthenticationManager can do one of three things in its authenticate() method:

  • return AuthenticationDetails if it can verify that the input represents a valid user.

  • throw LoginException if it cannot authenticate user with the passed credentials object.

  • throw UnsupportedCredentialsException if it does not support the passed credentials object.

The default implementation of AuthenticationManager is AuthenticationManagerBean, which delegates authentication to a chain of AuthenticationProvider instances. An AuthenticationProvider is an authentication module that can process a specific Credentials implementation, also it has a special method supports() to allow the caller to query if it supports a given Credentials type.

LoginProcedure
Figure 13. Standard user login process

Standard user login process:

  • The user enters their username and password.

  • Application client invokes Connection.login() method passing the user login and password.

  • Connection creates Credentials object and invokes login() method of AuthenticationService.

  • AuthenticationService delegates execution to the AuthenticationManager bean, which uses chain of AuthenticationProvider objects. There is LoginPasswordAuthenticationProvider that can work with LoginPasswordCredentials objects. It 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 the 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. BCrypt is used by default.

Built-in authentication providers

The platform contains the following implementations of AuthenticationProvider interface:

  • LoginPasswordAuthenticationProvider

  • RememberMeAuthenticationProvider

  • TrustedClientAuthenticationProvider

  • SystemAuthenticationProvider

  • AnonymousAuthenticationProvider

All the implementations load user from the database, verify the passed credentials object and create a non-active user session using UserSessionManager. That session instance can become active later in case of AuthenticationManager.login() is called.

LoginPasswordAuthenticationProvider, RememberMeAuthenticationProvider and TrustedClientAuthenticationProvider use additional pluggable checks: beans that implement UserAccessChecker interface. If at least one of the UserAccessChecker instances throw LoginException then authentication is considered failed and LoginException is thrown.

Besides, LoginPasswordAuthenticationProvider and RememberMeAuthenticationProvider check credentials instance using UserCredentialsChecker beans. There is only one built-in implementation of UserCredentialsChecker interface - BruteForceUserCredentialsChecker that checks if a user uses brute-force attack to find out valid credentials.

Exceptions

AuthenticationManager and AuthenticationProvider can throw LoginException or one of its descendants from authenticate() and login() methods. Also, UnsupportedCredentialsException is thrown if passed credentials object cannot be processed by available AuthenticationProvider beans.

See the following exception classes:

  • UnsupportedCredentialsException

  • LoginException

  • AccountLockedException

  • UserIpRestrictedException

  • RestApiAccessDeniedException

Events

Standard implementation of AuthenticationManager - AuthenticationManagerBean fires the following application events during login / authentication procedure:

  • BeforeAuthenticationEvent / AfterAuthenticationEvent

  • BeforeLoginEvent / AfterLoginEvent

  • AuthenticationSuccessEvent / AuthenticationFailureEvent

  • UserLoggedInEvent / UserLoggedOutEvent

  • UserSubstitutedEvent

Spring beans of the middle tier can handle these events using Spring @EventListener subscription:

@Component
public class LoginEventListener {
    @Inject
    private Logger log;

    @EventListener
    protected void onUserLoggedIn(UserLoggedInEvent event) {
        User user = event.getSource().getUser();
        log.info("Logged in user {}", user.getInstanceName());
    }
}

Event handlers of all events mentioned above (excluding AfterLoginEvent, UserSubstitutedEvent and UserLoggedInEvent) can throw LoginException to interrupt authentication / login process.

For instance, we can implement maintenance mode valve for our application that will block login attempts if maintenance mode is active.

@Component
public class MaintenanceModeValve {
    private volatile boolean maintenance = true;

    public boolean isMaintenance() {
        return maintenance;
    }

    public void setMaintenance(boolean maintenance) {
        this.maintenance = maintenance;
    }

    @EventListener
    protected void onBeforeLogin(BeforeLoginEvent event) throws LoginException {
        if (maintenance && event.getCredentials() instanceof AbstractClientCredentials) {
            throw new LoginException("Sorry, system is unavailable");
        }
    }
}
Extension points

You can extend authentication mechanisms using the following types of extension points:

  • AuthenticationService - replace existing AuthenticationServiceBean.

  • AuthenticationManager - replace existing AuthenticationManagerBean.

  • AuthenticationProvider implementations - implement additional or replace existing AuthenticationProvider.

  • Events - implement event handler.

You can replace existing beans using Spring Framework mechanisms, for instance by registering a new bean in Spring XML config of the core module.

<bean id="cuba_LoginPasswordAuthenticationProvider"
      class="com.company.authext.core.CustomLoginPasswordAuthenticationProvider"/>
public class CustomLoginPasswordAuthenticationProvider extends LoginPasswordAuthenticationProvider {
    @Inject
    public CustomLoginPasswordAuthenticationProvider(Persistence persistence, Messages messages) {
        super(persistence, messages);
    }

    @Override
    public AuthenticationDetails authenticate(Credentials credentials) throws LoginException {
        LoginPasswordCredentials loginPassword = (LoginPasswordCredentials) credentials;
        // for instance, add new check before login
        if ("demo".equals(loginPassword.getLogin())) {
            throw new LoginException("Demo account is disabled");
        }

        return super.authenticate(credentials);
    }
}

Event handlers can be ordered using the @Order annotation. All the platform beans and event handlers use order value between 100 and 1000, thus you can add your custom handling before or after the platform code. If you want to add your bean or event handler before platform beans - use a value lower than 100.

Ordering for an event handler:

@Component
public class DemoEventListener {
    @Inject
    private Logger log;

    @Order(10)
    @EventListener
    protected void onUserLoggedIn(UserLoggedInEvent event) {
        log.info("Demo");
    }
}

AuthenticationProviders can use Ordered interface and implement getOrder() method.

@Component
public class DemoAuthenticationProvider extends AbstractAuthenticationProvider
        implements AuthenticationProvider, Ordered {
    @Inject
    private UserSessionManager userSessionManager;

    @Inject
    public DemoAuthenticationProvider(Persistence persistence, Messages messages) {
        super(persistence, messages);
    }

    @Nullable
    @Override
    public AuthenticationDetails authenticate(Credentials credentials) throws LoginException {
        // ...
    }

    @Override
    public boolean supports(Class<?> credentialsClass) {
        return LoginPasswordCredentials.class.isAssignableFrom(credentialsClass);
    }

    @Override
    public int getOrder() {
        return 10;
    }
}
Additional Features
  • 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 AuthenticationService.login() method with TrustedClientCredentials. 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 AuthenticationManager.login() with SystemUserCredentials containing 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.

3.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.

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;
}));

3.2.12. Exceptions Handling

This section describes various aspects of working with exceptions in CUBA applications.

3.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.

3.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 {
...
3.2.12.3. Client-Level Exception Handlers

Unhandled exceptions thrown on the client tier or passed from Middleware, are passed to the special handlers mechanism of the Web Client block.

A handler is a managed bean implementing the UiExceptionHandler interface. Its handle() method should process the exception and return true, or immediately return false if this handler is not able to handle the passed exception. This behavior enables creating a "chain of responsibility" for handlers.

It is recommended to inherit your handlers from the AbstractUiExceptionHandler 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 a string array to the base constructor from the handler constructor. Each string of the array should contain one full class name of the handled exception.

Suppose you have the following exception class:

package com.company.demo.web;

public class ZeroBalanceException extends RuntimeException {

    public ZeroBalanceException() {
        super("Insufficient funds in your account");
    }
}

Then the handler for this exception must have the following constructor:

@Component("demo_ZeroBalanceExceptionHandler")
public class ZeroBalanceExceptionHandler extends AbstractUiExceptionHandler {

    public ZeroBalanceExceptionHandler() {
        super(ZeroBalanceException.class.getName());
    }
...

If the exception class is not accessible on the client side, specify its name with the string literal:

@Component("sample_ForeignKeyViolationExceptionHandler")
public class ForeignKeyViolationExceptionHandler extends AbstractUiExceptionHandler {

    public ForeignKeyViolationExceptionHandler() {
        super("java.sql.SQLIntegrityConstraintViolationException");
    }
...

In the case of using AbstractUiExceptionHandler as a base class, the processing logic is located in doHandle() method and looks as follows:

package com.company.demo.web;

import com.haulmont.cuba.gui.Notifications;
import com.haulmont.cuba.gui.exception.AbstractUiExceptionHandler;
import org.springframework.stereotype.Component;
import javax.annotation.Nullable;

@Component("demo_ZeroBalanceExceptionHandler")
public class ZeroBalanceExceptionHandler extends AbstractUiExceptionHandler {

    public ZeroBalanceExceptionHandler() {
        super(ZeroBalanceException.class.getName());
    }

    @Override
    protected void doHandle(String className, String message, @Nullable Throwable throwable, UiContext context) {
        context.getNotifications().create(Notifications.NotificationType.ERROR)
                .withCaption("Error")
                .withDescription(message)
                .show();
    }
}

If the name of the exception class is insufficient to make a decision whether this handler can be applied to the exception, define the canHandle() method. This method accepts also the text of the exception. If the handler is applicable for this exception, the method must return true. For example:

package com.company.demo.web.exceptions;

import com.haulmont.cuba.gui.Notifications;
import com.haulmont.cuba.gui.exception.AbstractUiExceptionHandler;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import javax.annotation.Nullable;

@Component("demo_ZeroBalanceExceptionHandler")
public class ZeroBalanceExceptionHandler extends AbstractUiExceptionHandler {

    public ZeroBalanceExceptionHandler() {
        super(ZeroBalanceException.class.getName());
    }

    @Override
    protected void doHandle(String className, String message, @Nullable Throwable throwable, UiContext context) {
        context.getNotifications().create(Notifications.NotificationType.ERROR)
                .withCaption("Error")
                .withDescription(message)
                .show();
    }

    @Override
    protected boolean canHandle(String className, String message, @Nullable Throwable throwable) {
        return StringUtils.containsIgnoreCase(message, "Insufficient funds in your account");
    }
}

The Dialogs interface available via the UiContext parameter of the doHandle() method provides a special dialog for displaying exceptions containing a collapsable area with the complete exception stack trace. This dialog is used in the default handler, but you can use it for your exceptions too, for example:

@Override
protected void doHandle(String className, String message, @Nullable Throwable throwable, UiContext context) {
    if (throwable != null) {
        context.getDialogs().createExceptionDialog()
                .withThrowable(throwable)
                .withCaption("Error")
                .withMessage(message)
                .show();
    } else {
        context.getNotifications().create(Notifications.NotificationType.ERROR)
                .withCaption("Error")
                .withDescription(message)
                .show();
    }
}

3.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 380 - Bean Validation 2.0 and its reference implementation: Hibernate Validator.

3.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, @Valid @NotNull Task task);
}

The @Valid annotation can be used if you need the cascaded validation of method parameters. In the example above, the constraints declared on the Task object will be validated as well.

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} or simply {msg://message_key} (for entities only). For example:

@Pattern(regexp = "\\S+@\\S+", message = "{msg://com.company.demo.entity/Customer.email.validationMsg}")
@Column(name = "EMAIL")
protected String email;

or, if the constraint is defined for an entity and the message is in the entity’s message pack:

@Pattern(regexp = "\\S+@\\S+", message = "{msg://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.

Validation Annotations Defined by CUBA

Apart from the standard annotations from the javax.validation.constraints package, you can use the following annotation defined in the CUBA platform:

  • @RequiredView - can be added to service method definitions to ensure that entity instances are loaded with all the attributes specified in a view. If the annotation is assigned to a method, then the return value is checked. If the annotation is assigned to a parameter, then this parameter is checked. If the return value or the parameter is a collection, all elements of the collection are checked. For example:

public interface MyService {
    String NAME = "sample_MyService";

    @Validated
    void processFoo(@RequiredView("foo-view") Foo foo);

    @Validated
    void processFooList(@RequiredView("foo-view") List<Foo> fooList);

    @Validated
    @RequiredView("bar-view")
    Bar loadBar(@RequiredView("foo-view") Foo foo);
}
3.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:

@UiController("sample_NewScreen")
@UiDescriptor("new-screen.xml")
public class NewScreen extends Screen {

    @Inject
    private TextField<String> field1;
    @Inject
    private TextField<String> field2;

    @Subscribe
    protected void onInit(InitEvent event) {
        field1.getValidators().stream()
                .filter(BeanPropertyValidator.class::isInstance)
                .forEach(field1::removeValidator); (1)

        field2.getValidators().stream()
                .filter(BeanPropertyValidator.class::isInstance)
                .forEach(validator -> {
                    ((BeanPropertyValidator) validator).setValidationGroups(new Class[] {UiComponentChecks.class}); (2)
                });
    }
}
1 Completely remove bean validation from the UI component.
2 Here validators will check only constraints with explicitly set UiComponentChecks group, because the Default group will not be passed.

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, @PastOrPresent, @Future, @FutureOrPresent annotations.

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 StandardEditor<Task> {
    @Subscribe
    protected void onInit(InitEvent event) {
        setCrossFieldValidate(false);
    }
}
Validation in DataManager

DataManager can perform validation of saved entity instances. The following parameters affect the validation:

  • The cuba.dataManagerBeanValidation application property sets the global default for whether the validation is performed.

  • You can override the global default by providing a CommitContext.ValidationMode value to CommitContext when using DataManager.commit() method or to DataContext.PreCommitEvent when saving data in a UI screen.

  • You can provide a list of validation groups to CommitContext and to DataContext.PreCommitEvent to apply only a subset of defined constraints.

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);
    // ...
}

3.2.14. Entity Attribute Access Control

The security subsystem allows you to set up access to entity attributes according to user permissions. That is the framework can automatically make an attribute read-only or hidden depending on a set of roles assigned to the current user. But sometimes you may want to change the access to attributes dynamically depending also on the current state of the entity or its linked entities.

The attribute access control mechanism allows you to create rules of what attributes should be hidden, read-only or required for a particular entity instance, and apply these rules automatically to Generic UI components and REST API.

The mechanism works as follows:

  • When DataManager loads an entity, it locates all managed beans implementing the SetupAttributeAccessHandler interface and invokes their setupAccess() method passing the SetupAttributeAccessEvent object. This object contains the loaded instance in the managed state, and three collections of attribute names: read-only, hidden and required (they are initially empty).

  • The SetupAttributeAccessHandler implementations analyze the state of the entity and fill collections of attribute names in the event appropriately. These classes are in fact containers for the rules that define the attribute access for a given instance.

  • The mechanism saves the attribute names, defined by your rules, in the entity instance itself (in a linked SecurityState object).

  • On the client tier, Generic UI and REST API use the SecurityState object to control the access to entity attributes.

In order to create a rule for particular entity type, do the following:

  • Create a managed bean in the core module of your project and implement the SetupAttributeAccessHandler interface. Parameterize the interface with the type of handled entity. The bean must have the default singleton scope. You have to implement the interface methods:

    • supports(Class) returns true if the handler is designed to work with the given entity class.

    • setupAccess(SetupAttributeAccessEvent) manipulates with the collections of attributes to set up the access. You should fill the collections of read-only, hidden and required attributes using the addHidden(), addReadOnly() and addRequired() methods of the event. The entity instance, which is available via the getEntity() method, is in the managed state, so you can safely access its attributes and attributes of its linked entities.

For example, provided that Order entity has customer and amount attributes, you could create the following rule for restricting access to the amount attribute depending on the customer:

package com.company.sample.core;

import com.company.sample.entity.Order;
import com.haulmont.cuba.core.app.SetupAttributeAccessHandler;
import com.haulmont.cuba.core.app.events.SetupAttributeAccessEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component("sample_OrderAttributeAccessHandler")
public class OrderAttributeAccessHandler implements SetupAttributeAccessHandler<Order> {

    @Override
    public boolean supports(Class clazz) {
        return Order.class.isAssignableFrom(clazz);
    }

    @Override
    public void setupAccess(SetupAttributeAccessEvent<Order> event) {
        Order order = event.getEntity();
        if (order.getCustomer() != null) {
            if ("PLATINUM".equals(order.getCustomer().getGrade().getCode())) {
                event.addHidden("amount");
            } else if ("GOLD".equals(order.getCustomer().getGrade().getCode())) {
                event.addReadOnly("amount");
            }
        }
    }
}
Attribute Access Control in Generic UI

The framework automatically applies attribute access restrictions to a screen at the moment between sending BeforeShowEvent and AfterShowEvent. If you don’t want to apply restrictions to a particular screen, add the @DisableAttributeAccessControl annotation to the controller class.

You may want to recompute and apply the restrictions while the screen is opened, in response of user actions. You can do it using the AttributeAccessSupport bean, passing the current screen and the entity which state has changed. For example:

@UiController("sales_Order.edit")
@UiDescriptor("order-edit.xml")
@EditedEntityContainer("orderDc")
@LoadDataBeforeShow
public class OrderEdit extends StandardEditor<Order> {

    @Inject
    private AttributeAccessSupport attributeAccessSupport;

    @Subscribe(id = "orderDc", target = Target.DATA_CONTAINER)
    protected void onOrderDcItemPropertyChange(InstanceContainer.ItemPropertyChangeEvent<Order> event) {
        if ("customer".equals(event.getProperty())) {
            attributeAccessSupport.applyAttributeAccess(this, true, getEditedEntity());
        }
    }

}

The second parameter of the applyAttributeAccess() method is a boolean value which specifies whether to reset components access to default before applying new restrictions. If it’s true, programmatic changes to the components state (if any) will be lost. When the method is invoked automatically on screen opening, the value of this parameter is false. But when invoking the method in response of UI events, set it to true, otherwise the restrictions on components will be summed and not replaced.

Attribute access restrictions are applied only to the components bound to single entity attributes, like TextField or LookupField. Table and other components implementing the ListComponent interface are not affected. So if you write a rule that can hide an attribute for some entity instances, we recommend not showing this attribute in tables at all.

3.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.

3.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 JDBC driver

HSQLDB

hsql

org.hsqldb.jdbc.JDBCDriver

PostgreSQL 8.4+

postgres

org.postgresql.Driver

Microsoft SQL Server 2005

mssql

2005

net.sourceforge.jtds.jdbc.Driver

Microsoft SQL Server 2008

mssql

com.microsoft.sqlserver.jdbc.SQLServerDriver

Microsoft SQL Server 2012+

mssql

2012

com.microsoft.sqlserver.jdbc.SQLServerDriver

Oracle Database 11g+

oracle

oracle.jdbc.OracleDriver

MySQL 5.6+

mysql

com.mysql.jdbc.Driver

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.

3.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'
    }
3.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

3.3.2. Connecting to a Non-Default Database Schema

PostgreSQL and Microsoft SQL Server support connection to a specific database schema. By default, the schema is public on PostgreSQL and dbo on SQL Server.

PostgreSQL

In order to use non-default schema on PostgreSQL, specify the currentSchema parameter in the connectionParams property of the createDb and updateDb Gradle tasks, for example:

task createDb(dependsOn: assembleDbScripts, type: CubaDbCreation) {
    dbms = 'postgres'
    host = 'localhost'
    dbName = 'my_db'
    connectionParams = '?currentSchema=my_schema'
    dbUser = 'cuba'
    dbPassword = 'cuba'
}

If you are using Studio, add this connection parameter to the Connection params field in the Project properties window. Studio will update build.gradle automatically. After that, you can update or re-create the database, and all tables will be created in the specified schema.

Microsoft SQL Server

On Microsoft SQL Server, providing a connection property is not enough, you have to link the schema with the database user. Below is an example of creating a new database and using a non-default schema in it.

  • Create a login:

    create login JohnDoe with password='saPass1'
  • Create a new database:

    create database my_db
  • Connect to the new database as sa, create a schema, then create a user and give him owner rights:

    create schema my_schema
    
    create user JohnDoe for login JohnDoe with default_schema = my_schema
    
    exec sp_addrolemember 'db_owner', 'JohnDoe'

Now you should specify the currentSchema parameter in the connectionParams property of the updateDb Gradle task (or in Studio project properties). In fact, this property is not handled by SQL Server JDBC driver, but it tells Studio and CUBA Gradle plugin what schema to use.

task updateDb(dependsOn: assembleDbScripts, type: CubaDbUpdate) {
    dbms = 'mssql'
    dbmsVersion = '2012'
    host = 'localhost'
    dbName = 'my_db'
    connectionParams = ';currentSchema=my_schema'
    dbUser = 'JohnDoe'
    dbPassword = 'saPass1'
}

Keep in mind, that you cannot re-create the SQL Server database from Studio or by executing createDb in the command line, because non-default schema requires association with a user. But if you run Update database in Studio or updateDb in the command line, all required tables will be created in the existing database and specified schema.

3.3.3. MS SQL Server Specifics

Microsoft SQL Server uses cluster indexes for tables.

By default, a clustered index is based on the table’s primary key, however, keys of the UUID type used by CUBA applications are poorly suited for clustered indexes. We recommend creating UUID primary keys with the nonclustered modificator:

create table SALES_CUSTOMER (
    ID uniqueidentifier not null,
    CREATE_TS datetime,
    ...
    primary key nonclustered (ID)
)^

3.3.4. MySQL Database Specifics

For the databases with the charset different from UTF-8, the framework scripts with constraints cannot be executed. In this case, modify the Charset and Collation name database properties by passing the following parameters in the Connection params field of the project properties:

?useUnicode=true&characterEncoding=UTF-8

MySQL does not support partial indexes, so the only way to implement a unique constraint for a soft deleted entity is to use the DELETE_TS column in the index. But there is another problem: MySQL allows multiple NULLs in a column with a unique constraint. Since the standard DELETE_TS column is nullable, it cannot be used in the unique index. We recommend the following workaround for creating unique constraints for soft deleted entities:

  1. Create a DELETE_TS_NN column in the database table. This column is not null and is initialized by a default value:

    create table DEMO_CUSTOMER (
        ...
        DELETE_TS_NN datetime(3) not null default '1000-01-01 00:00:00.000',
        ...
    )
  2. Create a trigger that will change DELETE_TS_NN value when DELETE_TS value is changed:

    create trigger DEMO_CUSTOMER_DELETE_TS_NN_TRIGGER before update on DEMO_CUSTOMER
    for each row
        if not(NEW.DELETE_TS <=> OLD.DELETE_TS) then
            set NEW.DELETE_TS_NN = if (NEW.DELETE_TS is null, '1000-01-01 00:00:00.000', NEW.DELETE_TS);
        end if
  3. Create a unique index including unique columns and DELETE_TS_NN:

    create unique index IDX_DEMO_CUSTOMER_UNIQ_NAME on DEMO_CUSTOMER (NAME, DELETE_TS_NN)

3.3.5. Scripts to Create and Update 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)

    Please note that Studio update scripts generator does not track changes of attributes' Column definition and custom datatype's sqlType. So if you changed them, create appropriate update scripts manually.

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. If you want to skip Groovy update scripts, you can run the following in command line:

delete from sys_db_changelog where script_name like '%groovy' and create_ts > (now() - interval '1 hour')

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.

3.3.5.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.

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)^
3.3.5.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.

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()
    }
})
3.3.5.3. 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 CUBA > Create database command in the 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 CUBA > Update database command in the 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.

3.3.5.4. Execution of Database Scripts by 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 Database in Production.

3.4. Middleware Components

The following figure shows the main components of the CUBA application middle tier.

Middleware
Figure 14. 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.

3.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.

3.4.1.1. Creating a Service

The name of service interface should end with Service, the names of implementation class – with ServiceBean.

CUBA Studio will help you to easily scaffold the service interface and class stubs. Studio will also register the new service in spring.xml automatically. To create a service, use the New > Service task in the Middleware node of CUBA project tree.

If you want to create a service manually, follow the steps below.

  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();
    }
}
3.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.

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.

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);
}
3.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.

3.4.2. Data Stores

A usual way of working with data in CUBA applications is manipulating entities - either declaratively through 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.).

CUBA Studio allows you to set up additional data stores on the Data Stores tab of the CUBA Project Properties window. 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.

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.

3.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.

    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).

3.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();
3.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.

3.4.4. ORM Layer

Object-Relational Mapping is a technology for linking relational database tables to programming language objects. CUBA uses the ORM implementation based on the EclipseLink framework.

ORM provides some obvious benefits:

  • 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.

At the same time, there are some drawbacks too. First of all, a developer working with ORM directly should have a good understanding of how it works. Also, ORM makes direct optimization of SQL and usage of the DBMS specifics difficult.

If you have any performance issues with the database access, the first thing to check is what SQL statements are actually executed. Use eclipselink.sql logger to output all SQL statements produced by ORM to the log file.

3.4.4.1. EntityManager

EntityManager – main ORM interface for working with persistent entities.

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. If no view is passed, the _local view is used by default.

  • 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 (for example, DataManager does this). However, if the loaded entity is cached, this view attribute is ignored and the entity will still be loaded with all local attributes.

3.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().

3.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.

3.4.4.4. Executing JPQL Queries

This section describes the Query interface which is designed to execute JPQL queries at the ORM level. 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’s have a look at the differences.

  • 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.

Using DELETE FROM with soft-deleted entities

The JPQL DELETE FROM statement throws an exception if launched for the soft-deleted entity and the Soft Delete mode is on. Such statement is actually transformed to SQL which deletes all instances not marked for deletion. This confusing behavior is disabled by default and can be enabled using the cuba.enableDeleteStatementInSoftDeleteMode application property.

Query Hints

The Query.setHint() method allows you to add some hints to the generated SQL statements. The hints are usually used to specify how the query should use indexes or other database specifics. The framework defines the following constants which can be passed to this method as hint names:

  • QueryHints.SQL_HINT - the hint value is added after the generated SQL statement. Provide the full hint string here, including comment delimiters if any.

  • QueryHints.MSSQL_RECOMPILE_HINT - adds OPTION(RECOMPILE) SQL hint for MS SQL Server database. The hint value is ignored.

When working with DataManager, query hints can be provided to the query using LoadContext.setHint() method.

3.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

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

3.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.

3.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) or @between(field_name, moment1, moment2, time_unit, user_timezone), 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.

  • user_timezone - an optional argument that defines the current user’s time zone to be considered in the query.

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 days, considering current user time zone:

select d from sales_Doc where @between(d.createTs, now-5, now, day, user_timezone)
@today

Has the format @today(field_name) or @today(field_name, user_timezone) 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) or @dateEquals(field_name, parameter, user_timezone) 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)

You can pass the current date using the now attribute. To set the days offset, use now with + or -, for example:

select d from sales_Doc where @dateEquals(d.createTs, now-1)
@dateBefore

Has the format @dateBefore(field_name, parameter) or @dateBefore(field_name, parameter, user_timezone) 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, user_timezone)

You can pass the current date using the now attribute. To set the days offset, use now with + or -, for example:

select d from sales_Doc where @dateBefore(d.createTs, now+1)
@dateAfter

Has the format @dateAfter(field_name, parameter) or @dateAfter(field_name, parameter, user_timezone) 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)

You can pass the current date using the now attribute. To set the days offset, use now with + or -, for example:

select d from sales_Doc where @dateAfter(d.createTs, now-1)
@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
3.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.

3.4.4.6. Entity Listeners

Entity Listeners are designed to react to the entity instances lifecycle events on the middle tier.

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 – after 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 must be a managed bean, so you can use injection in its fields and setters. Only one listener instance of a certain type is created for all instances of a particular entity class, therefore listener must not have a mutable state.

Be aware that for BeforeInsertEntityListener the framework guarantees managed state only for the root entity coming to the listener. Its references down to the object graph can be in the detached state. So you should use EntityManager.merge() method if you need to update these objects or EntityManager.find() to be able to access all their attributes. For example:

package com.company.sample.listener;

import com.company.sample.core.DiscountCalculator;
import com.company.sample.entity.*;
import com.haulmont.cuba.core.EntityManager;
import com.haulmont.cuba.core.listener.*;
import org.springframework.stereotype.Component;
import javax.inject.Inject;
import java.math.BigDecimal;

@Component("sample_OrderEntityListener")
public class OrderEntityListener implements
        BeforeInsertEntityListener<Order>,
        BeforeUpdateEntityListener<Order>,
        BeforeDeleteEntityListener<Order> {

    @Inject
    private DiscountCalculator discountCalculator; // a managed bean of the middle tier

    @Override
    public void onBeforeInsert(Order entity, EntityManager entityManager) {
        calculateDiscount(entity.getCustomer(), entityManager);
    }

    @Override
    public void onBeforeUpdate(Order entity, EntityManager entityManager) {
        calculateDiscount(entity.getCustomer(), entityManager);
    }

    @Override
    public void onBeforeDelete(Order entity, EntityManager entityManager) {
        calculateDiscount(entity.getCustomer(), entityManager);
    }

    private void calculateDiscount(Customer customer, EntityManager entityManager) {
        if (customer == null)
            return;

        // Delegate calculation to a managed bean of the middle tier
        BigDecimal discount = discountCalculator.calculateDiscount(customer.getId());

        // Merge customer instance because it comes to onBeforeInsert as part of another
        // entity's object graph and can be detached
        Customer managedCustomer = entityManager.merge(customer);

        // Set the discount for the customer. It will be saved on transaction commit.
        managedCustomer.setDiscount(discount);
    }
}

All listeners except BeforeAttachEntityListener work within a transaction. It means that if an exception is thrown inside the listener, the current transaction is rolled back and all database changes are discarded.

If you need to perform some actions after successful transaction commit, use Spring’s TransactionSynchronization callback to defer execution to a desired transaction phase. For example:

package com.company.sales.service;

import com.company.sales.entity.Customer;
import com.haulmont.cuba.core.EntityManager;
import com.haulmont.cuba.core.listener.BeforeInsertEntityListener;
import com.haulmont.cuba.core.listener.BeforeUpdateEntityListener;
import org.springframework.stereotype.Component;
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
import org.springframework.transaction.support.TransactionSynchronizationManager;

@Component("sales_CustomerEntityListener")
public class CustomerEntityListener implements BeforeInsertEntityListener<Customer>, BeforeUpdateEntityListener<Customer> {

    @Override
    public void onBeforeInsert(Customer entity, EntityManager entityManager) {
        printCustomer(entity);
    }

    @Override
    public void onBeforeUpdate(Customer entity, EntityManager entityManager) {
        printCustomer(entity);
    }

    private void printCustomer(Customer customer) {
        System.out.println("In transaction: " + customer);

        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
            @Override
            public void afterCommit() {
                System.out.println("After transaction commit: " + customer);
            }
        });
    }
}
Registration of entity listeners

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. This way you can add a listener to an entity located in an application component. In the example below, we add the entity listener implemented by the sample_UserEntityListener bean to the User entity defined in the framework:

    package com.company.sample.core;
    
    import com.haulmont.cuba.core.global.Events;
    import com.haulmont.cuba.core.sys.events.AppContextInitializedEvent;
    import com.haulmont.cuba.core.sys.listener.EntityListenerManager;
    import com.haulmont.cuba.security.entity.User;
    import org.springframework.context.event.EventListener;
    import org.springframework.core.annotation.Order;
    import org.springframework.stereotype.Component;
    import javax.inject.Inject;
    
    @Component("sample_AppLifecycle")
    public class AppLifecycle {
    
        @Inject
        private EntityListenerManager entityListenerManager;
    
        @EventListener(AppContextInitializedEvent.class) // notify after AppContext is initialized
        @Order(Events.LOWEST_PLATFORM_PRECEDENCE + 100)  // run after all framework listeners
        public void initEntityListeners() {
            entityListenerManager.addListener(User.class, "sample_UserEntityListener");
        }
    }

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.

3.4.5. Transaction Management

This section covers various aspects of transaction management in CUBA applications.

3.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.

3.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.

3.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 {
        methodB(); (1)
        tx.commit(); (4)
    } finally {
        tx.end();
    }
}

void methodB() {
    Transaction tx = persistence.getTransaction();
    try {
        tx.commit(); (2)
    } catch (Exception e) {
        return; (3)
    } finally {
        tx.end();
    }
}
1 calling a method creating a nested transaction
2 let us assume the exception occurs here
3 handle it and exit
4 at this point an exception will be thrown, because transaction is marked as rollback only

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();

        Employee employee = em.find(Employee.class, id); (1)
        assertEquals("old name", employee.getName());

        employee.setName("name A"); (2)

        methodB(); (3)

        tx.commit(); (8)
    } finally {
      tx.end();
    }
}

void methodB() {
    Transaction tx = persistence.getTransaction();
    try {
        EntityManager em = persistence.getEntityManager(); (4)

        Employee employee = em.find(Employee.class, id); (5)

        assertEquals("name A", employee.getName()); (6)
        employee.setName("name B");

        tx.commit(); (7)
    } finally {
      tx.end();
    }
}
1 loading an entity with name == "old name"
2 setting new value to the field
3 calling a method creating a nested transaction
4 retrieving the same instance of EntityManager as methodA
5 loading an entity with the same identifier
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
7 no actual commit is done at this point
8 the changes are committed to DB, and it will contain "name B"

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();

        Employee employee = em.find(Employee.class, id); (1)
        assertEquals("old name", employee.getName());

        employee.setName("name A"); (2)

        methodB(); (3)

        tx.commit(); (8)
    } finally {
      tx.end();
    }
}

void methodB() {
    Transaction tx = persistence.createTransaction();
    try {
        EntityManager em = persistence.getEntityManager(); (4)

        Employee employee = em.find(Employee.class, id); (5)

        assertEquals("old name", employee.getName()); (6)

        employee.setName("name B"); (7)

        tx.commit();
    } finally {
      tx.end();
    }
}
1 loading an entity with name == "old name"
2 setting new value to the field
3 calling a method creating a nested transaction
4 creating a new instance of EntityManager, as this is a new transaction
5 loading an entity with the same identifier
6 the field value is old because an old instance of the entity has been loaded from DB
7 the changes are committed to DB, and the value of "name B" will now be in DB
8 an exception occurs due to optimistic locking and commit will fail

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.

3.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.

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() {
...
3.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);
            }
        }
    }
}

3.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.

    If the entity cache is enabled, it is always recommended to increase the value of cache size. Otherwise, if the number of records returned by the query exceeds 100, a lot of fetch operations will be performed for each record of the query result.

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 CollectionLoader interface or cacheable XML attribute when working with data loaders.

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:

3.4.7. EntityChangedEvent

EntityChangedEvent is a Spring’s ApplicationEvent which is sent by the framework on the middle tier when an entity instance is saved to the database. The event can be handled both inside the transaction and after its completion (using @TransactionalEventListener).

The event is sent only for entities annotated with @PublishEntityChangedEvents. Do not forget to add this annotation to entities for which you want to listen to EntityChangedEvent.

EntityChangedEvent does not contain the changed entity instance but only its id. Also, the getOldValue(attributeName) method returns ids of references instead of objects. So if needed, the developer should reload entities with a required view and other parameters.

Below is an example of handling the EntityChangedEvent for a Customer entity in the current transaction and after its completion:

package com.company.demo.core;

import com.company.demo.entity.Customer;
import com.haulmont.cuba.core.app.events.AttributeChanges;
import com.haulmont.cuba.core.app.events.EntityChangedEvent;
import com.haulmont.cuba.core.entity.contracts.Id;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
import java.util.UUID;

@Component("demo_CustomerChangedListener")
public class CustomerChangedListener {

    @TransactionalEventListener(
            phase = TransactionPhase.BEFORE_COMMIT (1)
    )
    public void beforeCommit(EntityChangedEvent<Customer, UUID> event) {
        Id<Customer, UUID> entityId = event.getEntityId(); (2)
        EntityChangedEvent.Type changeType = event.getType(); (3)

        AttributeChanges changes = event.getChanges();
        if (changes.isChanged("name")) { (4)
            String oldName = changes.getOldValue("name"); (5)
            // ...
        }
    }

    @TransactionalEventListener(
            phase = TransactionPhase.AFTER_COMMIT (6)
    )
    public void afterCommit(EntityChangedEvent<Customer, UUID> event) {
        (7)
    }
}
1 - this listener is invoked inside the current transaction.
2 - changed entity’s id.
3 - change type: CREATED, UPDATED or DELETED.
4 - you can check if a particular attribute has been changed.
5 - you can get the old value of a changed attribute.
6 - this listener is invoked after the transaction is committed.
7 - after transaction commit, the event contains the same information as before commit.

If the listener is invoked inside the transaction, you can roll it back by throwing an exception. Nothing will be saved in the database then. If you don’t want any notifications to the user, use SilentException.

If an "after commit" listener throws an exception, it will be logged, but not propagated to the client (the user won’t see the error in UI).

When handling EntityChangedEvent in the current transaction (TransactionPhase.BEFORE_COMMIT), make sure you are using TransactionalDataManager to get the current state of the changed entity from the database. If you use DataManager, it will create a new transaction which may lead to a deadlock in the database if you try to read non-committed data.

In "after commit" listeners (TransactionPhase.AFTER_COMMIT), use DataManager or explicitly create a new transaction before using TransactionalDataManager.

Below is an example of using EntityChangedEvent to update related entities.

Suppose we have Order, OrderLine and Product entities as in the Quick Start Sales Application, but Product additionally has special boolean attribute and Order has numberOfSpecialProducts integer attribute which should be recalculated each time an OrderLine is created or deleted from the Order.

Create the following class with the @EventListener method which will be invoked for changed OrderLine entities before transaction commit:

package com.company.sales.listener;

import com.company.sales.entity.Order;
import com.company.sales.entity.OrderLine;
import com.haulmont.cuba.core.TransactionalDataManager;
import com.haulmont.cuba.core.app.events.EntityChangedEvent;
import com.haulmont.cuba.core.entity.contracts.Id;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

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

@Component("sales_OrderLineChangedListener")
public class OrderLineChangedListener {

    @Inject
    private TransactionalDataManager txDm;

    @TransactionalEventListener(
            phase = TransactionPhase.BEFORE_COMMIT
    )
    public void beforeCommit(EntityChangedEvent<OrderLine, UUID> event) {
        Order order;
        if (event.getType() != EntityChangedEvent.Type.DELETED) { (1)
            order = txDm.load(event.getEntityId()) (2)
                    .view("orderLine-with-order") (3)
                    .one()
                    .getOrder(); (4)
        } else {
            Id<Order, UUID> orderId = event.getChanges().getOldReferenceId("order"); (5)
            order = txDm.load(orderId).one();
        }

        long count = txDm.load(OrderLine.class) (6)
                .query("select o from sales_OrderLine o where o.order = :order")
                .parameter("order", order)
                .view("orderLine-with-product")
                .list().stream()
                .filter(orderLine -> Boolean.TRUE.equals(orderLine.getProduct().getSpecial()))
                .count();

        order.setNumberOfSpecialProducts((int) count);

        txDm.save(order); (7)
    }
}
1 - if OrderLine is not deleted, we can load it from the database by id.
2 - event.getEntityId() method returns id of the changed OrderLine.
3 - use a view that contains OrderLine together with the Order it belongs to. The view must contain the Order.numberOfSpecialProducts attribute because we need to update it later.
4 - get Order from the loaded OrderLine.
5 - if OrderLine has just been deleted, it cannot be loaded from the database, but event.getChanges() method returns all attributes of the entity, including identifiers of related entities. So we can load related Order by its id.
6 - load all OrderLine instances for the given Order, filter them by Product.special and count them. The view must contain OrderLine together with the related Product.
7 - save Order after changing its attribute.

3.4.8. EntityPersistingEvent

EntityPersistingEvent is a Spring’s ApplicationEvent which is sent by the framework on the middle tier before a new instance is saved to the database. At the moment of sending the event, an active transaction exists.

EntityPersistingEvent can be used to initialize entity attributes before creating it in the database:

package com.company.demo.core;

import com.company.demo.entity.Customer;
import com.haulmont.cuba.core.app.events.EntityPersistingEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component("demo_CustomerChangedListener")
public class CustomerChangedListener {

    @EventListener
    void beforePersist(EntityPersistingEvent<Customer> event) {
        Customer customer = event.getEntity();
        customer.setCode(obtainNewCustomerCode(customer));
    }

    // ...
}

3.4.9. 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.

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.

3.5. Generic User Interface

The Generic User Interface (Generic UI, GUI) framework allows you to create UI screens using Java and XML. XML is optional but it provides a declarative approach to the screen layout and reduces the amount of code which is required for building the user interface.

ClientStructure
Figure 15. The Structure of Generic User Interface

The application screens consist of the following parts:

  • Descriptors – XML files for declarative definition of the screen layout and data components.

  • Controllers – Java classes for handling events generated by the screen and its UI controls and for programmatic manipulation with the screen components.

The code of application screens interacts with visual component interfaces (VCL Interfaces). These interfaces are implemented using the Vaadin framework components.

Visual Components Library (VCL) contains a large set of ready-to-use components.

Data components provide a unified interface for binding visual components to entities and for working with entities in screen controllers.

Infrastructure includes the main application window and other common client mechanisms.

3.5.1. Screens and Fragments

A screen is a main unit of the generic UI. It contains visual components, data containers and non-visual components. A screen can be displayed inside the main application window either in the tab or as a modal dialog.

The main part of the screen is a Java or Groovy class called controller. Layout of the screen is usually defined in an XML file called descriptor.

In order to show a screen, the framework creates a new instance of the Window visual component, connects the window with the screen controller and loads the screen layout components as child components of the window. After that, the screen’s window is added to the main application window.

A fragment is another UI building block which can be used as part of screens and other fragments. It is very similar to screen internally, but has a specific lifecycle and the Fragment visual component instead of Window at the root of the components tree. Fragments also have controllers and XML descriptors.

3.5.1.1. Screen Controllers

A screen controller is a Java or Groovy class that contains the screen initialization and event handling logic. Normally, the controller is linked to an XML descriptor which defines the screen layout and data containers, but it can also create all visual and non-visual components programmatically.

All screen controllers implement the FrameOwner marker interface. The name of this interface means that it has a reference to a frame, which is a visual component representing the screen when it is shown in the main application window. There are two types of frames:

  • Window - a standalone window that can be displayed inside the main application window in a tab or as a modal dialog.

  • Fragment - a lightweight component that can be added to windows or other fragments.

Controllers are also divided into two distinct categories according to the frames they use:

  • Screen - a base class of window controllers.

  • ScreenFragment - a base class of fragment controllers.

screens
Figure 16. Controllers and Frames

The Screen class provides the most basic functionality for all standalone screens. There are also more specific base classes to use for screens working with entities:

  • StandardEditor - a base class for entity editor screens.

  • StandardLookup - a base class for entity browse and lookup screens.

  • MasterDetailScreen - a combined screen displaying the list of entities on the left and details of the selected entity on the right.

controller base classes
Figure 17. Base Classes of Controllers
3.5.1.1.1. Screen Controller Annotations

Class-level annotations on controllers are used to provide information about the screens to the framework. Some of the annotations are applicable to any type of screen, some of them should be used only on entity edit or lookup screens.

The following example demonstrates usage of common screen annotations:

package com.company.demo.web.screens;

import com.haulmont.cuba.gui.screen.*;

@UiController("demo_FooScreen")
@UiDescriptor("foo-screen.xml")
@LoadDataBeforeShow
@MultipleOpen
@DialogMode(forceDialog = true)
public class FooScreen extends Screen {
}
  • @UiController annotation indicates that the class is a screen controller. The value of the annotation is the id of the screen which can be used to refer to the screen from the main menu or when opening the screen programmatically.

  • @UiDescriptor annotation connects the screen controller to an XML descriptor. The value of the annotation specifies the path to the descriptor file. If the value contains a file name only, it is assumed that the file is located in the same package as the controller class.

  • @LoadDataBeforeShow annotation indicates that all data loaders should be triggered automatically before showing the screen. More precisely, the data is loaded after invoking all BeforeShowEvent listeners but before AfterShowEvent listeners. If you need to perform some actions when loading data before the screen is shown, remove this annotation or set its value to false and use getScreenData().loadAll() method or load() methods of individual loaders in a BeforeShowEvent listener. Consider also using the DataLoadCoordinator facet for fine-grained data loading control.

  • @MultipleOpen annotation indicates that the screen can be opened from the main menu multiple times. By default, when a user clicks a main menu item, the framework checks if the screen of the same class and id is already opened on top of a main window tab. If such screen is found, it is closed and the new instance of the screen is opened in a new tab. When the @MultipleOpen annotation is present, no checks are performed and a new instance of the screen is simply opened in the new tab.

    You can provide your own way of checking if the screen instance is the same by overriding the isSameScreen() method in the screen controller.

  • @DialogMode annotation allows you to specify geometry and behavior of the screen when it is opened in the dialog window. It corresponds to the <dialogMode> element of the screen descriptor and can be used instead. Settings in XML have priority over the annotation for all parameters except forceDialog. The forceDialog parameter is joined: when it is set to true either in the annotation or in XML, the screen is always opened in a dialog.

Example of annotations specific to lookup screens:

package com.company.demo.web.screens;

import com.haulmont.cuba.gui.screen.*;
import com.company.demo.entity.Customer;

// common annotations
@UiController("demo_Customer.browse")
@UiDescriptor("customer-browse.xml")
@LoadDataBeforeShow
// lookup-specific annotations
@LookupComponent("customersTable")
@PrimaryLookupScreen(Customer.class)
public class CustomerBrowse extends StandardLookup<Customer> {
}
  • @LookupComponent annotation specifies the id of a UI component to be used for getting a value returned from the lookup.

    Instead of using the annotation, you can specify the lookup component programmatically by overriding the getLookupComponent() method in the screen controller.

  • @PrimaryLookupScreen annotation indicates that this screen is the default lookup screen for entities of the specified type. The annotation has greater priority than the {entity_name}.lookup / {entity_name}.browse name convention.

Example of annotations specific to editor screens:

package com.company.demo.web.data.sort;

import com.haulmont.cuba.gui.screen.*;
import com.company.demo.entity.Customer;

// common annotations
@UiController("demo_Customer.edit")
@UiDescriptor("customer-edit.xml")
@LoadDataBeforeShow
// editor-specific annotations
@EditedEntityContainer("customerDc")
@PrimaryEditorScreen(Customer.class)
public class CustomerEdit extends StandardEditor<Customer> {
}
  • @EditedEntityContainer annotation specifies a data container that contains the edited entity.

    Instead of using the annotation, you can specify the container programmatically by overriding the getEditedEntityContainer() method in the screen controller.

  • @PrimaryEditorScreen annotation indicates that this screen is the default edit screen for entities of the specified type. The annotation has greater priority than the {entity_name}.edit name convention.

3.5.1.1.2. Screen Events

This section describes the screen lifecycle events that can be handled in controllers.

  • InitEvent is sent when the screen controller and all its declaratively defined components are created, and dependency injection is completed. Nested fragments are not initialized yet. Some visual components are not fully initialized, for example buttons are not linked with actions.

    @Subscribe
    protected void onInit(InitEvent event) {
        Label<String> label = uiComponents.create(Label.TYPE_STRING);
        label.setValue("Hello World");
        getWindow().add(label);
    }
  • AfterInitEvent is sent when the screen controller and all its declaratively defined components are created, dependency injection is completed, and all components have completed their internal initialization procedures. Nested screen fragments (if any) have sent their InitEvent and AfterInitEvent. In this event listener, you can create visual and data components and perform additional initialization if it depends on initialized nested fragments.

  • InitEntityEvent is sent in screens inherited from StandardEditor and MasterDetailScreen before the new entity instance is set to edited entity container. Use this event listener to initialize default values in the new entity instance, for example:

    @Subscribe
    protected void onInitEntity(InitEntityEvent<Foo> event) {
        event.getEntity().setStatus(Status.ACTIVE);
    }
  • BeforeShowEvent is sent right before the screen is shown, i.e. it is not added to the application UI yet. Security restrictions are applied to UI components. Saved component settings are not yet applied to UI components. Data is not loaded yet for screens annotated with @LoadDataBeforeShow. In this event listener, you can load data, check permissions and modify UI components. For example:

    @Subscribe
    protected void onBeforeShow(BeforeShowEvent event) {
        customersDl.load();
    }
  • AfterShowEvent is sent right after the screen is shown, i.e. when it is added to the application UI. Saved component settings are applied to UI components. In this event listener, you can show notifications, dialogs or other screens. For example:

    @Subscribe
    protected void onAfterShow(AfterShowEvent event) {
        notifications.create().withCaption("Just opened").show();
    }
  • BeforeCloseEvent is sent right before the screen is closed by its close(CloseAction) method. The screen is still displayed and fully functional. Component settings are not saved yet. In this event listener, you can check any conditions and prevent screen closing using the preventWindowClose() method of the event, for example:

    @Subscribe
    protected void onBeforeClose(BeforeCloseEvent event) {
        if (Strings.isNullOrEmpty(textField.getValue())) {
            notifications.create().withCaption("Input required").show();
            event.preventWindowClose();
        }
    }

    There is also an event with the same name but defined in the Window interface. It is sent before the screen is closed by an external (relative to the controller) action, like clicking on the button in the window tab or by pressing the Esc key. The way the window is closed can be obtained using the getCloseOrigin() method which returns a value implementing the CloseOrigin interface. Its default implementation CloseOriginType has three values:

    • BREADCRUMBS - the screen is closed by clicking on the breadcrumbs link.

    • CLOSE_BUTTON - the screen is closed by the close button in the window header or by the window tab close button or context menu actions: Close, Close All, Close Others.

    • SHORTCUT - the screen is closed by the keyboard shortcut defined in the cuba.gui.closeShortcut application property.

      You can subscribe to Window.BeforeCloseEvent by specifying Target.FRAME in the @Subscribe annotation:

      @Subscribe(target = Target.FRAME)
      protected void onBeforeClose(Window.BeforeCloseEvent event) {
          if (event.getCloseOrigin() == CloseOriginType.BREADCRUMBS) {
              event.preventWindowClose();
          }
      }
  • AfterCloseEvent is sent after the screen is closed by its close(CloseAction) method and after Screen.AfterDetachEvent. Component settings are saved. In this event listener, you can show notifications or dialogs after closing the screen, for example:

    @Subscribe
    protected void onAfterClose(AfterCloseEvent event) {
        notifications.create().withCaption("Just closed").show();
    }
  • AfterDetachEvent is sent after the screen is removed from the application UI when it is closed by the user or when the user logs out. This event listener can be used for releasing resources acquired by the screen. Note that this event is not sent on HTTP session expiration.

  • UrlParamsChangedEvent is sent when browser URL parameters corresponding to opened screen are changed. It is fired before the screen is shown, which enables to do some preparatory work. In this event listener, you can load some data or change screen controls state depending on new parameters:

    @Subscribe
    protected void onUrlParamsChanged(UrlParamsChangedEvent event) {
        Map<String, String> params = event.getParams();
        // handle new params
    }
3.5.1.1.3. ScreenFragment Events

This section describes the lifecycle events that can be handled in fragment controllers.

  • InitEvent is sent when the fragment controller and all its declaratively defined components are created, and dependency injection is completed. Nested fragments are not initialized yet. Some visual components are not fully initialized, for example buttons are not linked with actions. If the fragment is attached to the host screen declaratively in XML, this event is sent after InitEvent of the host controller. Otherwise it is sent when the fragment is added to the host’s component tree.

  • AfterInitEvent is sent when the fragment controller and all its declaratively defined components are created, dependency injection is completed, and all components have completed their internal initialization procedures. Nested screen fragments (if any) have sent their InitEvent and AfterInitEvent. In this event listener, you can create visual and data components and perform additional initialization if it depends on initialized nested fragments.

  • AttachEvent is sent when the fragment is added to the host’s component tree. At this moment, the fragment is fully initialized, InitEvent and AfterInitEvent have been sent. In this event listener, you can access the host screen using getHostScreen() and getHostController() methods.

  • DetachEvent is sent when the fragment is programmatically removed from the host’s component tree. You cannot access the host screen in this event listener.

An example of listening to fragment events:

@UiController("demo_AddressFragment")
@UiDescriptor("address-fragment.xml")
public class AddressFragment extends ScreenFragment {

    private static final Logger log = LoggerFactory.getLogger(AddressFragment.class);

    @Subscribe
    private void onAttach(AttachEvent event) {
        Screen hostScreen = getHostScreen();
        FrameOwner hostController = getHostController();
        log.info("onAttach to screen {} with controller {}", hostScreen, hostController);
    }

    @Subscribe
    private void onDetach(DetachEvent event) {
        log.info("onDetach");
    }
}

In a fragment controller, you can also subscribe to events of the host screen by specifying the PARENT_CONTROLLER value in the target attribute of the annotation, for example:

@Subscribe(target = Target.PARENT_CONTROLLER)
private void onBeforeShowHost(Screen.BeforeShowEvent event) {
    //
}

Any event can be handled this way, including InitEntityEvent sent by entity editors.

3.5.1.2. Screen XML Descriptors

Screen descriptor is an XML file containing declarative definition of visual components, data components and some screen parameters.

A descriptor has the window the root element.

The root element attributes:

  • class − name of a controller class.

  • messagesPack − a default message pack for the screen. It is used to obtain localized messages in the controller using getMessage() method and in the XML descriptor using message key without specifying the pack.

  • caption − window caption, can contain a link to a message from the above mentioned pack, for example,

    caption="msg://credits"
  • focusComponent − identifier of a component which should get input focus when the screen is displayed.

Elements of the descriptor:

  • data − defines data components of the screen.

  • dialogMode - defines the settings of geometry and behavior of the screen when it is opened as a dialog.

    Attributes of dialogMode:

    • closeable - defines whether the dialog window has close button. Possible values: true, false.

    • closeOnClickOutside - defines if the dialog window should be closed by click on outside the window area, when the window has a modal mode. Possible values: true, false.

    • forceDialog - specifies that the screen should always be opened as a dialog regardless of what WindowManager.OpenType was selected in the calling code. Possible values: true, false.

    • height - sets the height of the dialog window.

    • maximized - if the true value is set, the dialog window will be maximized across the screen. Possible values: true, false.

    • modal - specifies the modal mode for the dialog window. Possible values: true, false.

    • positionX - sets the x position of the top-left corner of the dialog window.

    • positionY - sets the y position of the top-left corner of the dialog window.

    • resizable - defines whether the user can change the size of the dialog window. Possible values: true, false.

    • width - sets the width of the dialog window.

    For example:

    <dialogMode height="600"
                width="800"
                positionX="200"
                positionY="200"
                forceDialog="true"
                closeOnClickOutside="false"
                resizable="true"/>
  • actions – defines the list of actions for the screen.

  • timers – defines the list of timers for the screen.

  • layout − root element of the screen layout.

3.5.1.3. Opening Screens

A screen can be opened from the main menu, by navigating to a URL or programmatically from another screen. In this section, we explain how to open screens programmatically.



Using the Screens interface

The Screens interface allows you to create and show screens of any type.

Suppose we have a screen to show a message with some special formatting:

Screen controller
@UiController("demo_FancyMessageScreen")
@UiDescriptor("fancy-message-screen.xml")
@DialogMode(forceDialog = true, width = "300px")
public class FancyMessageScreen extends Screen {

    @Inject
    private Label<String> messageLabel;

    public void setFancyMessage(String message) { (1)
        messageLabel.setValue(message);
    }

    @Subscribe("closeBtn")
    protected void onCloseBtnClick(Button.ClickEvent event) {
        closeWithDefaultAction();
    }
}
1 - a screen parameter
Screen descriptor
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd" caption="Fancy Message">
    <layout>
        <label id="messageLabel" value="A message" stylename="h1"/>
        <button id="closeBtn" caption="Close"/>
    </layout>
</window>

Then we can create and open it from another screen as follows:

@Inject
private Screens screens;

private void showFancyMessage(String message) {
    FancyMessageScreen screen = screens.create(FancyMessageScreen.class);
    screen.setFancyMessage(message);
    screens.show(screen);
}

Notice how we create the screen instance, provide a parameter for it and then show the screen.

If the screen does not require any parameters from the caller code, you can create and open it in one line:

@Inject
private Screens screens;

private void showDefaultFancyMessage() {
    screens.create(FancyMessageScreen.class).show();
}

Screens is not a Spring bean, so you can only inject it to screen controllers or obtain using ComponentsHelper.getScreenContext(component).getScreens() static method.

Using the ScreenBuilders bean

The ScreenBuilders bean allows you to open all kinds of screens with various parameters. Below is an example of using it for opening a screen and executing some code after the screen is closed (see more details here):

@Inject
private ScreenBuilders screenBuilders;
@Inject
private Notifications notifications;

private void openOtherScreen() {
    screenBuilders.screen(this)
            .withScreenClass(OtherScreen.class)
            .withAfterCloseListener(e -> {
                notifications.create().withCaption("Closed").show();
            })
            .build()
            .show();
}

Next we’ll consider working with editor and lookup screens.

Example of opening a default editor for the Customer entity instance:

@Inject
private ScreenBuilders screenBuilders;

private void editSelectedEntity(Customer entity) {
    screenBuilders.editor(Customer.class, this)
            .editEntity(entity)
            .build()
            .show();
}

In this case, the editor will update the entity, but the caller screen will not receive the updated instance.

The most common case is when you need to edit an entity displayed by some Table or DataGrid component. Then you should use the following form of invocation, which is more concise and automatically updates the table:

@Inject
private GroupTable<Customer> customersTable;
@Inject
private ScreenBuilders screenBuilders;

private void editSelectedEntity() {
    screenBuilders.editor(customersTable).build().show();
}

In order to create a new entity instance and open the editor screen for it, just call the newEntity() method on the builder:

@Inject
private GroupTable<Customer> customersTable;
@Inject
private ScreenBuilders screenBuilders;

private void createNewEntity() {
    screenBuilders.editor(customersTable)
            .newEntity()
            .build()
            .show();
}

The default editor screen is determined by the following procedure:

  1. If an editor screen annotated with @PrimaryEditorScreen exists, it is used.

  2. Otherwise, an editor screen with {entity_name}.edit id is used (for example, sales_Customer.edit).

The builder provides a lot of methods to set optional parameters of the opened screen. For example, the following code creates an entity first initializing the new instance, in a particular editor opened as a dialog:

@Inject
private GroupTable<Customer> customersTable;
@Inject
private ScreenBuilders screenBuilders;

private void editSelectedEntity() {
    screenBuilders.editor(customersTable).build().show();
}

private void createNewEntity() {
    screenBuilders.editor(customersTable)
            .newEntity()
            .withInitializer(customer -> {          // lambda to initialize new instance
                customer.setName("New customer");
            })
            .withScreenClass(CustomerEdit.class)    // specific editor screen
            .withLaunchMode(OpenMode.DIALOG)        // open as modal dialog
            .build()
            .show();
}

Entity lookup screens can also be opened with various parameters.

Below is an example of opening a default lookup screen of the User entity:

@Inject
private TextField<String> userField;
@Inject
private ScreenBuilders screenBuilders;

private void lookupUser() {
    screenBuilders.lookup(User.class, this)
            .withSelectHandler(users -> {
                User user = users.iterator().next();
                userField.setValue(user.getName());
            })
            .build()
            .show();
}

If you need to set the looked up entity to a field, use the more concise form:

@Inject
private PickerField<User> userPickerField;
@Inject
private ScreenBuilders screenBuilders;

private void lookupUser() {
    screenBuilders.lookup(User.class, this)
            .withField(userPickerField)     // set result to the field
            .build()
            .show();
}

The default lookup screen is determined by the following procedure:

  1. If a lookup screen annotated with @PrimaryLookupScreen exists, it is used.

  2. Otherwise, if a screen with {entity_name}.lookup id exists, it is used (for example, sales_Customer.lookup).

  3. Otherwise, a screen with {entity_name}.browse id is used (for example, sales_Customer.browse).

As with edit screens, use the builder methods to set optional parameters of the opened screen. For example, the following code looks up the User entity using a particular lookup screen opened as a dialog:

@Inject
private TextField<String> userField;
@Inject
private ScreenBuilders screenBuilders;

private void lookupUser() {
    screenBuilders.lookup(User.class, this)
            .withScreenId("sec$User.browse")          // specific lookup screen
            .withLaunchMode(OpenMode.DIALOG)        // open as modal dialog
            .withSelectHandler(users -> {
                User user = users.iterator().next();
                userField.setValue(user.getName());
            })
            .build()
            .show();
}
Passing parameters to screens

The recommended way of passing parameters to an opened screen is to use public setters of the screen controller, as demonstrated in the example above.

With this approach, you can pass parameters to screens of any type, including entity edit and lookup screens opened using ScreenBuilders or from the main menu. The invocation of the same FancyMessageScreen using ScreenBuilders with passing the parameter looks as follows:

@Inject
private ScreenBuilders screenBuilders;

private void showFancyMessage(String message) {
    FancyMessageScreen screen = screenBuilders.screen(this)
            .withScreenClass(FancyMessageScreen.class)
            .build();
    screen.setFancyMessage(message);
    screen.show();
}

Another way is to define a special class for parameters and pass its instance to the standard withOptions() method of the screen builder. The parameters class must implement the ScreenOptions marker interface. For example:

import com.haulmont.cuba.gui.screen.ScreenOptions;

public class FancyMessageOptions implements ScreenOptions {

    private String message;

    public FancyMessageOptions(String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}

In the opened FancyMessageScreen screen, the options can be obtained in InitEvent and AfterInitEvent handlers:

@Subscribe
private void onInit(InitEvent event) {
    ScreenOptions options = event.getOptions();
    if (options instanceof FancyMessageOptions) {
        String message = ((FancyMessageOptions) options).getMessage();
        messageLabel.setValue(message);
    }
}

The invocation of the FancyMessageScreen screen using ScreenBuilders with passing ScreenOptions looks as follows:

@Inject
private ScreenBuilders screenBuilders;

private void showFancyMessage(String message) {
    screenBuilders.screen(this)
            .withScreenClass(FancyMessageScreen.class)
            .withOptions(new FancyMessageOptions(message))
            .build()
            .show();
}

As you can see, this approach requires type casting in the controller receiving the parameters, so use it wisely and prefer the type-safe setters approach explained above.

Usage of the ScreenOptions object is the only way to get parameters if the screen is opened from a screen based on the legacy API. In this case, the options object is of type MapScreenOptions and you can handle it in the opened screen as follows:

@Subscribe
private void onInit(InitEvent event) {
    ScreenOptions options = event.getOptions();
    if (options instanceof MapScreenOptions) {
        String message = (String) ((MapScreenOptions) options).getParams().get("message");
        messageLabel.setValue(message);
    }
}
Executing code after close and returning values

Each screen sends AfterCloseEvent when it closes. You can add a listener to a screen to be notified when the screen is closed, for example:

@Inject
private Screens screens;
@Inject
private Notifications notifications;

private void openOtherScreen() {
    OtherScreen otherScreen = screens.create(OtherScreen.class);
    otherScreen.addAfterCloseListener(afterCloseEvent -> {
        notifications.create().withCaption("Closed " + afterCloseEvent.getScreen()).show();
    });
    otherScreen.show();
}

When using ScreenBuilders, the listener can be provided in the withAfterCloseListener() method:

@Inject
private ScreenBuilders screenBuilders;
@Inject
private Notifications notifications;

private void openOtherScreen() {
    screenBuilders.screen(this)
            .withScreenClass(OtherScreen.class)
            .withAfterCloseListener(afterCloseEvent -> {
                notifications.create().withCaption("Closed " + afterCloseEvent.getScreen()).show();
            })
            .build()
            .show();
}

The event object provides an information about how the screen was closed: its getCloseAction() method returns an object with the CloseAction interface. The FrameOwner interface implemented by screen controllers contains a few constants defining CloseAction implementations used by the framework. In the application, you can use these constants or create your own implementations.

Consider a simple custom screen:

package com.company.demo.web.screens;

import com.haulmont.cuba.gui.components.Button;
import com.haulmont.cuba.gui.screen.*;

@UiController("demo_OtherScreen")
@UiDescriptor("other-screen.xml")
public class OtherScreen extends Screen {

    private String result;

    public String getResult() {
        return result;
    }

    @Subscribe("okBtn")
    private void onOkBtnClick(Button.ClickEvent event) {
        result = "Done";
        close(WINDOW_COMMIT_AND_CLOSE_ACTION); (1)
    }

    @Subscribe("cancelBtn")
    private void onCancelBtnClick(Button.ClickEvent event) {
        closeWithDefaultAction(); (2)
    }
}
1 - on "OK" button click, set some result state and close the screen with standard WINDOW_COMMIT_AND_CLOSE_ACTION action.
2 - on "Cancel" button click, close the with a default action.

Now in the AfterCloseEvent listener we can analyze how the screen was closed, and read the result value if needed:

@Inject
private ScreenBuilders screenBuilders;
@Inject
private Notifications notifications;

private void openOtherScreen() {
        screenBuilders.screen(this)
                .withScreenClass(OtherScreen.class)
                .withAfterCloseListener(afterCloseEvent -> {
                    OtherScreen otherScreen = afterCloseEvent.getScreen();
                    if (afterCloseEvent.getCloseAction().equals(WINDOW_COMMIT_AND_CLOSE_ACTION)) {
                        String result = otherScreen.getResult();
                        notifications.create().withCaption("Result: " + result).show();
                    }
                })
                .build()
                .show();
}

Another way of returning values from screens is using custom CloseAction implementations. Let’s rewrite the above example to use the following action class:

package com.company.demo.web.screens;

import com.haulmont.cuba.gui.screen.StandardCloseAction;

public class MyCloseAction extends StandardCloseAction {

    private String result;

    public MyCloseAction(String result) {
        super("myCloseAction");
        this.result = result;
    }

    public String getResult() {
        return result;
    }
}

Then we can use this action when closing the screen:

@Inject
private Screens screens;
@Inject
private Notifications notifications;

private void openOtherScreen() {
    Screen otherScreen = screens.create("demo_OtherScreen", OpenMode.THIS_TAB);
    otherScreen.addAfterCloseListener(afterCloseEvent -> {
        CloseAction closeAction = afterCloseEvent.getCloseAction();
        if (closeAction instanceof MyCloseAction) {
            String result = ((MyCloseAction) closeAction).getResult();
            notifications.create().withCaption("Result: " + result).show();
        }
    });
    otherScreen.show();
}

As you can see, when values are returned through a custom CloseAction, the caller doesn’t have to know the opened screen class because it doesn’t invoke methods of the concrete screen controller. So the screen can be created by its string id.

Of course, the same approach for returning values through close actions can be used when opening screens using ScreenBuilders.

3.5.1.4. Using Screen Fragments

In this section, we explain how to define and use screen fragments. See also ScreenFragment Events for how to handle fragment lifecycle events.



Declarative usage of a fragment

Suppose we have a fragment for entering an address:

AddressFragment.java
@UiController("demo_AddressFragment")
@UiDescriptor("address-fragment.xml")
public class AddressFragment extends ScreenFragment {
}
address-fragment.xml
<fragment xmlns="http://schemas.haulmont.com/cuba/screen/fragment.xsd">
    <layout>
        <textField id="cityField" caption="City"/>
        <textField id="zipField" caption="Zip"/>
    </layout>
</fragment>

Then we can include it to another screen using the fragment element with the screen attribute pointing to the fragment id, specified in its @UiController annotation:

host-screen.xml
<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd"
        caption="Some Screen">
    <layout>
        <groupBox id="addressBox" caption="Address">
            <fragment screen="demo_AddressFragment"/>
        </groupBox>
    </layout>
</window>

The fragment element can be added to any UI-container of the screen, including the top-level layout element.

Programmatic usage of a fragment

The same fragment can be included in the screen programmatically in a InitEvent or AfterInitEvent handler as follows:

host-screen.xml
<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd"
        caption="Some Screen">
    <layout>
        <groupBox id="addressBox" caption="Address"/>
    </layout>
</window>
HostScreen.java
@UiController("demo_HostScreen")
@UiDescriptor("host-screen.xml")
public class HostScreen extends Screen {

    @Inject
    private Fragments fragments; (1)

    @Inject
    private GroupBoxLayout addressBox;

    @Subscribe
    private void onInit(InitEvent event) {
        AddressFragment addressFragment = fragments.create(this, AddressFragment.class); (2)
        addressBox.add(addressFragment.getFragment()); (4)
    }
}
1 - inject the Fragments bean which is designed to instantiate screen fragments
2 - create the fragment’s controller by its class
3 - get the Fragment visual component from the controller and add it to a UI-container

If the fragment has parameters, you can set them via public setters prior to adding the fragment to the screen. Then the parameters will be available in InitEvent and AfterInitEvent handlers of the fragment controller.

Passing parameters to fragments

A fragment controller can have public setters to accept parameters as it is done when opening screens. If the fragment is opened programmatically, the setters can be invoked explicitly:

@UiController("demo_HostScreen")
@UiDescriptor("host-screen.xml")
public class HostScreen extends Screen {

    @Inject
    private Fragments fragments;

    @Inject
    private GroupBoxLayout addressBox;

    @Subscribe
    private void onInit(InitEvent event) {
        AddressFragment addressFragment = fragments.create(this, AddressFragment.class);
        addressFragment.setStrParam("some value"); (1)
        addressBox.add(addressFragment.getFragment());
    }
}
1 - pass a parameter before adding the fragment to the screen.

If the fragment is added to the screen declaratively in XML, use properties element to pass the parameters, for example:

<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd"
        caption="Some Screen">
    <data>
        <instance id="someDc" class="com.company.demo.entity.Demo"/>
    </data>
    <layout>
        <textField id="someField"/>
        <fragment screen="demo_AddressFragment">
            <properties>
                <property name="strParam" value="some value"/> (1)
                <property name="dataContainerParam" ref="someDc"/> (2)
                <property name="componentParam" ref="someField"/> (3)
            </properties>
        </fragment>
    </layout>
</window>
1 - pass a string parameter to setStrParam() method.
2 - pass a data container to setDataContainerParam() method.
3 - pass the TextField component to setComponentParam() method.

Use the value attribute to specify values and the ref attribute to specify identifiers of the screen components. Setters must have parameters of appropriate types.

Data components in screen fragments

A screen fragment can have its own data containers and loaders, defined in the data XML element. At the same time, the framework creates a single instance of DataContext for the screen and all its fragments. Therefore all loaded entities are merged to the same context and their changes are saved when the host screen is committed.

In the following example, we consider usage of own data containers and loaders in a screen fragment.

Suppose we have a City entity and in the fragment, instead of the text field, we want to show a drop-down list with available cities. We can define data components in the fragment descriptor as we would in a regular screen:

address-fragment.xml
<fragment xmlns="http://schemas.haulmont.com/cuba/screen/fragment.xsd">
    <data>
        <collection id="citiesDc" class="com.company.demo.entity.City" view="_base">
            <loader id="citiesLd">
                <query><![CDATA[select e from demo_City e ]]></query>
            </loader>
        </collection>
    </data>
    <layout>
        <lookupField id="cityField" caption="City" optionsContainer="citiesDc"/>
        <textField id="zipField" caption="Zip"/>
    </layout>
</fragment>

In order to load data in the fragment when the host screen is opened, we need to subscribe to the screen’s event:

AddressFragment.java
@UiController("demo_AddressFragment")
@UiDescriptor("address-fragment.xml")
public class AddressFragment extends ScreenFragment {

    @Inject
    private CollectionLoader<City> citiesLd;

    @Subscribe(target = Target.PARENT_CONTROLLER) (1)
    private void onBeforeShowHost(Screen.BeforeShowEvent event) {
        citiesLd.load();
    }
}
1 - subscribing to BeforeShowEvent of the host screen

The @LoadDataBeforeShow annotation does not work for screen fragments.

Provided data containers

The next example demonstrates how to use data containers of the host screen in the fragment.

host-screen.xml
<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd"
        caption="Some Screen">
    <data>
        <instance id="addressDc" class="com.company.demo.entity.Address"/> (1)
    </data>
    <layout>
        <groupBox id="addressBox" caption="Address">
            <fragment screen="demo_AddressFragment"/>
        </groupBox>
    </layout>
</window>
1 - data container which is used in the fragment below
address-fragment.xml
<fragment xmlns="http://schemas.haulmont.com/cuba/screen/fragment.xsd">
    <data>
        <instance id="addressDc" class="com.company.demo.entity.Address"
                  provided="true"/> (1)

        <collection id="citiesDc" class="com.company.demo.entity.City" view="_base">
            <loader id="citiesLd">
                <query><![CDATA[select e from demo_City e]]></query>
            </loader>
        </collection>
    </data>
    <layout>
        <lookupField id="cityField" caption="City" optionsContainer="citiesDc"
                     dataContainer="addressDc" property="city"/> (2)
        <textField id="zipField" caption="Zip"
                   dataContainer="addressDc" property="zip"/>
    </layout>
</fragment>
1 - provided="true" means that the container with the same id must exist in a host screen or enclosing fragment, i.e it must be provided from outside
2 - UI-components are linked to the provided data container

In the XML element having provided="true", all attributes except id are ignored but can be present to provide information for design time tools.

3.5.1.5. Screen Mixins

Mixins enable creating features that can be reused in multiple UI screens without the need to inherit your screens from common base classes. Mixins are implemented using Java interfaces with default methods.

Mixins have the following characteristics:

  • A screen can have multiple mixins.

  • A mixin interface can subscribe to screen events.

  • A mixin can save some state in the screen if needed.

  • A mixin can obtain screen components and infrastructure beans like Dialogs, Notifications, etc.

  • In order to parameterize its behavior, a mixin can rely on screen annotations or introduce abstract methods to be implemented by the screen.

Usage of mixins is normally as simple as implementing specific interfaces in a screen controller. In the example below, the CustomerEditor screen acquires functionality of mixins implemented by the HasComments, HasHistory, HasAttachments interfaces:

public class CustomerEditor extends StandardEditor<Customer>
                            implements HasComments, HasHistory, HasAttachments {
    // ...
}

A mixin can use the following classes to work with screen and the infrastructure:

  • com.haulmont.cuba.gui.screen.Extensions provides static methods for saving and retrieving a state from the screen where the mixin is used, as well as access to BeanLocator which in turn allows you to get any Spring managed bean.

  • UiControllerUtils provides access to the screen’s UI and data components.

Below are examples that demonstrate how to create and use mixins.

Banner mixin

This is a very simple mixin that shows a label on top of the screen.

package com.company.demo.web.mixins;

import com.haulmont.cuba.core.global.BeanLocator;
import com.haulmont.cuba.gui.UiComponents;
import com.haulmont.cuba.gui.components.Label;
import com.haulmont.cuba.gui.screen.*;
import com.haulmont.cuba.web.theme.HaloTheme;

public interface HasBanner {

    @Subscribe
    default void initBanner(Screen.InitEvent event) {
        BeanLocator beanLocator = Extensions.getBeanLocator(event.getSource()); (1)
        UiComponents uiComponents = beanLocator.get(UiComponents.class); (2)

        Label<String> banner = uiComponents.create(Label.TYPE_STRING); (3)
        banner.setStyleName(HaloTheme.LABEL_H2);
        banner.setValue("Hello, world!");

        event.getSource().getWindow().add(banner, 0); (4)
    }
}
1 - get BeanLocator.
2 - get factory of UI components.
3 - create Label and set its properties.
4 - add label to the screen’s root UI component.

The mixin can be used in a screen as follows:

package com.company.demo.web.customer;

import com.company.demo.web.mixins.HasBanner;
import com.haulmont.cuba.gui.screen.*;
import com.company.demo.entity.Customer;

@UiController("demo_Customer.edit")
@UiDescriptor("customer-edit.xml")
// ...
public class CustomerEdit extends StandardEditor<Customer> implements HasBanner {
    // ...
}
DeclarativeLoaderParameters mixin

The next mixin helps to establish master-detail relationships between data containers. Normally, you have to subscribe to ItemChangeEvent of the master container and set a parameter to the detail’s loader, as described in Dependencies Between Data Components. The mixin will do it automatically if the parameter has a special name pointing to the master container.

The mixin will use a state object to pass information between event handlers. It’s done mostly for demonstration purposes because we could put all the logic in a single BeforeShowEvent handler.

First, let’s create a class for the shared state. It contains a single field for storing a set of loaders to be triggered in the BeforeShowEvent handler:

package com.company.demo.web.mixins;

import com.haulmont.cuba.gui.model.DataLoader;
import java.util.Set;

public class DeclarativeLoaderParametersState {

    private Set<DataLoader> loadersToLoadBeforeShow;

    public DeclarativeLoaderParametersState(Set<DataLoader> loadersToLoadBeforeShow) {
        this.loadersToLoadBeforeShow = loadersToLoadBeforeShow;
    }

    public Set<DataLoader> getLoadersToLoadBeforeShow() {
        return loadersToLoadBeforeShow;
    }
}

Next, create the mixin interface:

package com.company.demo.web.mixins;

import com.haulmont.cuba.gui.model.*;
import com.haulmont.cuba.gui.screen.*;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public interface DeclarativeLoaderParameters {

    Pattern CONTAINER_REF_PATTERN = Pattern.compile(":(container\\$(\\w+))");

    @Subscribe
    default void onDeclarativeLoaderParametersInit(Screen.InitEvent event) { (1)
        Screen screen = event.getSource();
        ScreenData screenData = UiControllerUtils.getScreenData(screen); (2)

        Set<DataLoader> loadersToLoadBeforeShow = new HashSet<>();

        for (String loaderId : screenData.getLoaderIds()) {
            DataLoader loader = screenData.getLoader(loaderId);
            String query = loader.getQuery();
            Matcher matcher = CONTAINER_REF_PATTERN.matcher(query);
            while (matcher.find()) { (3)
                String paramName = matcher.group(1);
                String containerId = matcher.group(2);
                InstanceContainer<?> container = screenData.getContainer(containerId);
                container.addItemChangeListener(itemChangeEvent -> { (4)
                    loader.setParameter(paramName, itemChangeEvent.getItem()); (5)
                    loader.load();
                });
                if (container instanceof HasLoader) { (6)
                    loadersToLoadBeforeShow.add(((HasLoader) container).getLoader());
                }
            }
        }

        DeclarativeLoaderParametersState state =
                new DeclarativeLoaderParametersState(loadersToLoadBeforeShow); (7)
        Extensions.register(screen, DeclarativeLoaderParametersState.class, state);
    }

    @Subscribe
    default void onDeclarativeLoaderParametersBeforeShow(Screen.BeforeShowEvent event) { (8)
        Screen screen = event.getSource();
        DeclarativeLoaderParametersState state =
                Extensions.get(screen, DeclarativeLoaderParametersState.class);
        for (DataLoader loader : state.getLoadersToLoadBeforeShow()) {
            loader.load(); (9)
        }
    }
}
1 - subscribe to InitEvent.
2 - get the ScreenData object where all data containers and loaders defined in XML are registered.
3 - check if a loader parameter matches the :container$masterContainerId pattern.
4 - extract the master container id from the parameter name and register a ItemChangeEvent listener for this container.
5 - reload the detail loader for the new master item.
6 - add the master loader to set to trigger it later in the BeforeShowEvent handler.
7 - create the shared state object and store it in the screen using Extensions utility class.
8 - subscribe to BeforeShowEvent.
9 - trigger all master loaders found in the InitEvent handler.

In the screen XML descriptor, define master and detail containers and loaders. The detail’s loader should have a parameter with the name like :container$masterContainerId:

<collection id="countriesDc"
            class="com.company.demo.entity.Country" view="_local">
    <loader id="countriesDl">
        <query><![CDATA[select e from demo_Country e]]></query>
    </loader>
</collection>
<collection id="citiesDc"
            class="com.company.demo.entity.City" view="city-view">
    <loader id="citiesDl">
        <query><![CDATA[
        select e from demo_City e
        where e.country = :container$countriesDc
        ]]></query>
    </loader>
</collection>

In the screen controller, just add the mixin interface, and it will trigger the loaders appropriately:

package com.company.demo.web.country;

import com.company.demo.entity.Country;
import com.company.demo.web.mixins.DeclarativeLoaderParameters;
import com.haulmont.cuba.gui.screen.*;

@UiController("demo_Country.browse")
@UiDescriptor("country-browse.xml")
@LookupComponent("countriesTable")
public class CountryBrowse extends StandardLookup<Country>
                           implements DeclarativeLoaderParameters {
}
3.5.1.6. Root Screens

A root screen is a Generic UI screen which is displayed directly in the web browser tab. There are two types of such screens: login screen and main screen. Among other components, any root screen can contain the WorkArea component which enables opening other application screens in the inner tabs. If the root screen doesn’t contain WorkArea, application screens can be opened only in DIALOG mode.

Login screen

Login screen is displayed before a user logs in. You can customize the login screen by extending the one provided by the framework or by creating completely new screen from scratch.

In order to extend the existing screen, use Login screen template in Studio screen creation wizard. As a result, Studio will create a screen extending the standard login screen. This screen will be used instead of the standard one because it will have the same login identifier in the @UiController annotation.

If you want to create a new login screen from scratch, use Blank screen template. The source code of a minimalistic login screen may look as follows:

my-login-screen.xml
<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd"
        caption="Login"
        messagesPack="com.company.sample.web">
    <layout>
        <label value="Hello World"/>
        <button id="loginBtn" caption="Login"/>
    </layout>
</window>
MyLoginScreen.java
package com.company.sample.web;

import com.haulmont.cuba.gui.Route;
import com.haulmont.cuba.gui.components.Button;
import com.haulmont.cuba.gui.screen.*;
import com.haulmont.cuba.security.auth.LoginPasswordCredentials;
import com.haulmont.cuba.web.App;

@UiController("myLogin")
@UiDescriptor("my-login-screen.xml")
@Route(path = "login", root = true)
public class MyLoginScreen extends Screen {

    @Subscribe("loginBtn")
    private void onLoginBtnClick(Button.ClickEvent event) {
        App.getInstance().getConnection().login(
                new LoginPasswordCredentials("admin", "admin"));
    }
}

In order to use this login screen instead of the default one, set its id to the cuba.web.loginScreenId property in web-app.properties file:

cuba.web.loginScreenId = myLogin

You could as well give your screen the default login id and don’t change this property.

Main screen

Main screen is the root application screen displayed when the user is logged in. The standard main screen with side menu provided by the framework has main id.

Studio has a number of templates for creating a customized main screen. All of them use the same MainScreen base class for controllers.

  • Main screen with side menu creates an extension of the standard main screen with main id.

  • Main screen with responsive side menu creates a similar screen, but the side menu is responsive and collapses on narrow displays. The screen will have an own generated id which must be registered in web-app.properties:

    cuba.web.mainScreenId = respSideMenuMainScreen
  • Main screen with top menu creates a screen with top menu bar and ability to show folders panel on the left. The screen will have an own generated id which must be registered in web-app.properties:

    cuba.web.mainScreenId = topMenuMainScreen

The following special components may be used in the main screen in addition to the standard UI components:

  • SideMenu - application menu in the form of the vertical tree.

  • AppMenu – application menu bar.

  • AppWorkArea – work area, the required component for opening screens in the THIS_TAB, NEW_TAB and NEW_WINDOW modes.

  • FoldersPane – a panel for application and search folders.

  • UserIndicator – the field which displays the name of the current user, as well as enables selecting substituted users, if any.

    The setUserNameFormatter() method allows you to represent the user’s name in a format different from the User instance name:

    userIndicator.setUserNameFormatter(value -> value.getName() + " - [" + value.getEmail() + "]");
    userIndicator
  • NewWindowButton – the button which opens a new main screen in a separate browser tab.

  • UserActionsButton – if the session is not authenticated, shows the link to the login screen. Otherwise, shows the menu with the link to the user settings screen and logout action.

    You can install LoginHandler or LogoutHandler in the main screen controller to implement your custom logic:

    @Install(to = "userActionsButton", subject = "loginHandler")
    private void loginHandler(UserActionsButton.LoginHandlerContext ctx) {
        // do custom logic
    }
    
    @Install(to = "userActionsButton", subject = "logoutHandler")
    private void logoutHandler(UserActionsButton.LogoutHandlerContext ctx) {
        // do custom logic
    }
  • LogoutButton – the application logout button.

  • TimeZoneIndicator – the label displaying the current user’s time zone.

  • FtsField – the full text search field.

The following application properties may affect the main screen:

3.5.2. Visual Components Library

3.5.2.1. Components

Menu

AppMenu

gui_AppMenu

SideMenu

gui_sidemenu

Buttons

Button

Button

PopupButton

PopupButton

LinkButton

LinkButton

Text

Label

gui_label

Text inputs

TextField

gui_textField_data

PasswordField

gui_PasswordField

MaskedField

gui_MaskedField

TextArea

gui_TextArea

ResizableTextArea

gui_textField_resizable

RichTextArea

gui_RichTextArea

SourceCodeEditor

gui_SourceCodeEditor_1

Date inputs

DateField

gui_dateField

DatePicker

gui_datepicker_mini

TimeField

gui_timeField

Selects

CheckBox

CheckBox

CheckBoxGroup

gui_CheckBoxGroup

LookupField

LookupField

LookupPickerField

LookupPickerField

OptionsGroup

gui_optionsGroup

OptionsList

gui_optionsList

PickerField

PickerField

RadioButtonGroup

gui_RadioButtonGroup

SearchPickerField

gui_searchPickerField

SuggestionPickerField

gui_suggestionPickerField_1

TwinColumn

TwinColumn

Uploads

FileUploadField

Upload

FileMultiUploadField

Tables and trees

DataGrid

gui_dataGrid

Table

gui_table

GroupTable

gui_groupTable

TreeDataGrid

gui_TreeDataGrid

TreeTable

gui_treeTable

Tree

gui_Tree

Others

BrowserFrame

gui_browserFrame

BulkEditor

gui_invoiceBulkEdit

Calendar

gui_calendar_1

CapsLockIndicator

gui_capsLockIndicator

ColorPicker

gui_color_picker

FieldGroup

gui_fieldGroup

Filter

gui_filter_mini

Form

gui_Form_1

Image

gui_Image_1

PopupView

gui_popup_view_mini_open

TokenList

gui_tokenList

3.5.2.1.1. AppMenu

AppMenu component provides means of customizing the main menu in the main screen and managing menu items dynamically.

gui AppMenu

CUBA Studio has some templates for the main window based on the standard MainScreen provided by the platform. In the example below the template extends the MainScreen class and provides direct access to the AppMenu instance:

public class ExtMainScreen extends MainScreen implements Window.HasFoldersPane {

    @Inject
    private Notifications notifications;
    @Inject
    private AppMenu mainMenu;

    @Subscribe
    public void onInit(InitEvent event) {
        AppMenu.MenuItem item = mainMenu.createMenuItem("shop", "Shop");
        AppMenu.MenuItem subItem = mainMenu.createMenuItem("customer", "Customers", null, menuItem -> {
            notifications.create()
                    .withCaption("Customers menu item clicked")
                    .withType(Notifications.NotificationType.HUMANIZED)
                    .show();
        });
        item.addChildItem(subItem);
        mainMenu.addMenuItem(item, 0);
    }
}

Methods of the AppMenu interface:

  • addMenuItem() - adds menu item to the end of root items list or to specified position in the root items list.

  • createMenuItem() - the factory method that creates new menu item. Does not add item to the menu. id must be unique for whole menu.

  • createSeparator() - creates menu separator.

  • getMenuItem()/getMenuItemNN() - returns the item from the menu tree by its id.

  • getMenuItems() - returns the list of root menu items.

  • hasMenuItems() - returns true if the menu has items.

Methods of the MenuItem interface:

  • addChildItem() / removeChildItem() - adds/removes the menu item to the end or to the specified position of children list.

  • getCaption() - returns the String caption of the menu item.

  • getChildren() - returns the list of child items.

  • setCommand() - sets item command, or the action to be performed on this menu item click.

  • setDescription() - sets the String description of the menu item, displayed as a popup tip.

  • setIconFromSet() - sets the item’s icon.

  • getId() - returns the menu item id.

  • getMenu() - returns the menu item owner.

  • setStylename() - sets one or more user-defined style names of the component, replacing any previous user-defined styles. Multiple styles can be specified as a space-separated list of style names. The style names must be valid CSS class names.

  • hasChildren() - returns true if the menu item has child items.

  • isSeparator() - returns true if the item is a separator.

  • setVisible() - manages visibility of the menu item.

The appearance of the AppMenu component can be customized using SCSS variables with $cuba-menubar-* and $cuba-app-menubar-* prefixes. You can change these variables in the visual editor after creating a theme extension or a custom theme.



3.5.2.1.2. BrowserFrame

A BrowserFrame is designed to display embedded web pages. It is the equivalent of the HTML iframe element.

gui browserFrame

Component’s XML-name: browserFrame

An example of component definition in an XML-descriptor of a screen:

<browserFrame id="browserFrame"
              height="280px"
              width="600px"
              align="MIDDLE_CENTER">
    <url url="https://www.cuba-platform.com/blog/cuba-7-the-new-chapter"/>
</browserFrame>

Similarly to the Image component, the BrowserFrame component can also display images from different resources. You can set the resource type declaratively using the browserFrame elements listed below:

  • classpath - a resource in the classpath.

    <browserFrame>
        <classpath path="com/company/sample/web/screens/myPic.jpg"/>
    </browserFrame>
  • file - a resource in the file system.

    <browserFrame>
        <file path="D:\sample\modules\web\web\VAADIN\images\myImage.jpg"/>
    </browserFrame>
  • relativePath - a resource in the application directory.

    <browserFrame>
        <relativePath path="VAADIN/images/myImage.jpg"/>
    </browserFrame>
  • theme - a theme resource, for example:

    <browserFrame>
        <theme path="../halo/com.company.demo/myPic.jpg"/>
    </browserFrame>
  • url - a resource which can be loaded from the given URL.

    <browserFrame>
        <url url="http://www.foobar2000.org/"/>
    </browserFrame>

browserFrame attributes:

  • The allow attribute specifies Feature Policy for the component. The value of the attribute should be a space-separated list of allowed features:

    • autoplay – controls whether the current document is allowed to autoplay media requested through the interface.

    • camera – controls whether the current document is allowed to use video input devices.

    • document-domain – controls whether the current document is allowed to set document.domain.

    • encrypted-media – controls whether the current document is allowed to use the Encrypted Media Extensions API (EME).

    • fullscreen – controls whether the current document is allowed to use Element.requestFullScreen().

    • geolocation – controls whether the current document is allowed to use the Geolocation Interface.

    • microphone – controls whether the current document is allowed to use audio input devices.

    • midi – controls whether the current document is allowed to use the Web MIDI API.

    • payment – controls whether the current document is allowed to use the Payment Request API.

    • vr – controls whether the current document is allowed to use the WebVR API.

  • alternateText - sets an alternate text for the frame in case the resource is not set or unavailable.

  • The referrerpolicy attribute indicates which referrer to send when fetching the frame’s resource. ReferrerPolicy – enum of standard values of the attribute:

    • no-referrer – the referer header will not be sent.

    • no-referrer-when-downgrade – the referer header will not be sent to origins without TLS (HTTPS).

    • origin – the sent referrer will be limited to the origin of the referring page: its scheme, host, and port.

    • origin-when-cross-origin – the referrer sent to other origins will be limited to the scheme, the host, and the port. Navigation on the same origin will still include the path.

    • same-origin – a referrer will be sent for the same origin, but cross-origin requests will contain no referrer information.

    • strict-origin – only sends the origin of the document as the referrer when the protocol security level stays the same (HTTPS->HTTPS), but doesn’t send it to a less secure destination (HTTPS->HTTP).

    • strict-origin-when-cross-origin – sends a full URL when performing a same-origin request, only sends the origin when the protocol security level stays the same (HTTPS->HTTPS), and sends no header to a less secure destination (HTTPS->HTTP).

    • unsafe-url – the referrer will include the origin and the path. This value is unsafe because it leaks origins and paths from TLS-protected resources to insecure origins.

  • The sandbox attribute applies extra restrictions to the content in the frame. The value of the attribute should be either empty to apply all restrictions or space-separated tokens to lift particular restrictions. Sandbox – enum of standard values of the attribute:

    • allow-forms – allows the resource to submit forms.

    • allow-modals – lets the resource open modal windows.

    • allow-orientation-lock – lets the resource lock the screen orientation.

    • allow-pointer-lock – lets the resource use the Pointer Lock API.

    • allow-popups – allows popups (such as window.open(), target="_blank", or showModalDialog()).

    • allow-popups-to-escape-sandbox – lets the sandboxed document open new windows without those windows inheriting the sandboxing.

    • allow-presentation – lets the resource start a presentation session.

    • allow-same-origin – allows the iframe content to be treated as being from the same origin.

    • allow-scripts – lets the resource run scripts.

    • allow-storage-access-by-user-activation – lets the resource request access to the parent’s storage capabilities with the Storage Access API.

    • allow-top-navigation – lets the resource navigate the top-level browsing context (the one named _top).

    • allow-top-navigation-by-user-activation – lets the resource navigate the top-level browsing context, but only if initiated by a user gesture.

    • allow-downloads-without-user-activation – allows for downloads to occur without a gesture from the user.

    • "" – applies all restrictions.

  • The srcdoc attribute – inline HTML to embed, overriding the src attribute. The IE and Edge browsers don’t support this attribute. You can also specify a value for the srcdoc attribute using the srcdocFile attribute in xml by passing the path to the file with HTML code.

  • The srcdocFile attribute – the path to the file whose contents will be set to the srcdoc attribute. File content is obtained by using the classPath resource. You can set the attribute value only in the XML descriptor.

browserFrame resources settings:

  • bufferSize - the size of the download buffer in bytes used for this resource.

    <browserFrame>
        <file bufferSize="1024" path="C:/img.png"/>
    </browserFrame>
  • cacheTime - the length of cache expiration time in milliseconds.

    <browserFrame>
        <file cacheTime="2400" path="C:/img.png"/>
    </browserFrame>
  • mimeType - the MIME type of the resource.

    <browserFrame>
        <url url="https://avatars3.githubusercontent.com/u/17548514?v=4&#38;s=200"
             mimeType="image/png"/>
    </browserFrame>

Methods of the BrowserFrame interface:

  • addSourceChangeListener() - adds a listener that will be notified when the content source is changed.

    @Inject
    private Notifications notifications;
    @Inject
    BrowserFrame browserFrame;
    
    @Subscribe
    protected void onInit(InitEvent event) {
        browserFrame.addSourceChangeListener(sourceChangeEvent ->
                notifications.create()
                        .withCaption("Content updated")
                        .show());
    }
  • setSource() - sets the content source for the frame. The method accepts the resource type and returns the resource object that can be configured using the fluent interface. Each resource type has its own methods, for example, setPath() for ThemeResource type or setStreamSupplier() for StreamResource type:

    BrowserFrame frame = uiComponents.create(BrowserFrame.NAME);
    try {
        frame.setSource(UrlResource.class).setUrl(new URL("http://www.foobar2000.org/"));
    } catch (MalformedURLException e) {
        throw new RuntimeException(e);
    }

    You can use the same resource types as for the Image component.

  • createResource() - creates the frame resource implementation by its type. The created object can be later passed to the setSource() method.

    UrlResource resource = browserFrame.createResource(UrlResource.class)
            .setUrl(new URL(fromString));
    browserFrame.setSource(resource);
HTML markup in BrowserFrame:

BrowserFrame can be used to integrate HTML markup into your application. For example, you can dynamically generate HTML content at runtime from the user input.

<textArea id="textArea"
          height="250px"
          width="400px"/>
<browserFrame id="browserFrame"
              height="250px"
              width="500px"/>
textArea.addTextChangeListener(event -> {
    byte[] bytes = event.getText().getBytes(StandardCharsets.UTF_8);

    browserFrame.setSource(StreamResource.class)
            .setStreamSupplier(() -> new ByteArrayInputStream(bytes))
            .setMimeType("text/html");
});
gui browserFrame 2
PDF in BrowserFrame:

Besides HTML, BrowserFrame can be also used for displaying PDF files content. Use the file as a resource and set the corresponding MIME type for it:

@Inject
private BrowserFrame browserFrame;
@Inject
private Resources resources;

@Subscribe
protected void onInit(InitEvent event) {
    browserFramePdf.setSource(StreamResource.class)
            .setStreamSupplier(() -> resources.getResourceAsStream("/com/company/demo/" +
                    "web/screens/CUBA_Hands_on_Lab_6.8.pdf"))
            .setMimeType("application/pdf");
}
gui browserFrame 3


3.5.2.1.3. Button

A button performs an action when a user clicks on it.

Button

Component’s XML-name: button

Buttons can contain a caption, an icon, or both. The figure below shows different button types.

gui buttonTypes

An example of a button with a tooltip and a caption retrieved from a localized message pack:

<button id="textButton" caption="msg://someAction" description="Press me"/>

The button’s caption is set using the caption attribute, the tooltip – using the description attribute.

If the disableOnClick attribute is set to true the button will be automatically disabled when clicked, typically to prevent (accidental) extra clicks on a button. You can later return the button to the enabled state by invoking the setEnabled(true) method.

The icon attribute defines icon location in theme catalog or the icon name in the icon set. Detailed information on recommended icon placement is available in Icons.

Example of creating a button with an icon:

<button id="iconButton" caption="" icon="SAVE"/>

The button’s main function is to perform an action on a click. Controller method that should be invoked after a click can be defined using invoke attribute. The attribute value should contain name of the controller method satisfying the following conditions:

  • The method should be public.

  • The method should return void.

  • The method should not have any arguments, or should have a single argument of Component type. If the method has a Component argument, then an instance of the invoking button will be passed in it.

Below is the example of a button invoking someMethod:

<button invoke="someMethod" caption="msg://someButton"/>

A method named someMethod should be defined in the screen controller:

public void someMethod() {
    //some actions
}

The invoke attribute is ignored if action attribute is set. The action attribute contains the name of action corresponding to the button.

Example of a button with an action:

<actions>
    <action id="someAction" caption="msg://someAction"/>
</actions>
<layout>
    <button action="someAction"/>
</layout>

Any action present in the component implementing Component.ActionsHolder interface can be assigned to a button. This applies to Table, GroupTable, TreeTable, Tree. The way of adding actions (declaratively in the XML descriptor or programmatically in the controller) is irrelevant. In any case, for using an action, the name of the component and the identifier of the required action must be specified in the action attribute, separated by dot. For instance, in the next example the create action of the coloursTable table is assigned to a button:

<button action="coloursTable.create"/>

Button actions can also be created programmatically in the screen controller by deriving them from BaseAction class.

If an Action instance is defined for a Button, the button will take the following properties from it: caption, description, icon, enable, visible. The caption, description and icon properties will be imported from Action only if they are not set in the Button itself. All other listed Action properties have priority over the Button properties.

If Action properties are changed after the Action has been set for a Button, then Button properties also change accordingly, i.e. the button listens to the changes in Action properties. In this case, the caption, description and icon properties will change even if they had been initially assigned to the button itself.

Button styles

The primary attribute is used to set the highlighting for the button. The highlighting is applied by default if the action invoked by this button is primary.

<button primary="true"
        invoke="foo"/>

The highlighting is available by default in the Hover theme; to enable this feature in a Halo-based theme, set true for the $cuba-highlight-primary-action style variable.

actions primary

Next, in Web Client with a Halo-based theme, you can set predefined styles to the Button component using the stylename attribute either in the XML descriptor or in the screen controller:

<button id="button"
        caption="Friendly button"
        stylename="friendly"/>

When setting a style programmatically, select one of the HaloTheme class constants with the BUTTON_ prefix:

button.setStyleName(HaloTheme.BUTTON_FRIENDLY);
  • borderless - borderless button.

  • borderless-colored - borderless button with a colored caption text.

  • danger - a prominent button that can be used when the action is considered unsafe for the user (i.e. it causes data loss or some other irreversible action).

  • friendly - a prominent button that can be used for primary actions when the action is considered safe for the user (i.e. does not cause any data loss or any other irreversible action).

  • icon-align-right - align the icon to the right side of the button caption.

  • icon-align-top - stack the icon on top of the button caption.

  • icon-only - only show the icon in the button, and size the button to a square shape.

  • primary - primary action button (e.g. the button that should get activated when the user presses the Enter key in a form). Use sparingly, only one default button per view should be visible.

  • quiet - "quiet" button, which looks like borderless until you hover over it with the mouse.

The appearance of the Button component can be customized using SCSS variables with $cuba-button-* prefix. You can change these variables in the visual editor after creating a theme extension or a custom theme.



3.5.2.1.4. BulkEditor

BulkEditor is a component that enables changing attribute values for several entity instances at once. The component is a button, usually added to a table or a tree, which opens the entity bulk editor on click.

gui bulkEdit

XML-name of the component: bulkEditor

BulkEditor works only in screens based on legacy API. The similar functionality for the current API is provided by the BulkEditAction.

To enable the use of BulkEditor, the table or tree must have the multiselect attribute set to "true".

The entity editor is automatically generated based on the defined view (containing the fields of this entity, including references), the entity’s dynamic attributes (if any) and the user permissions. System attributes are not displayed in the editor either.

Entity attributes in the editor are sorted alphabetically. By default, the fields are empty. At screen commit, non-empty attribute values defined in the editor, are set for all the entity instances.

The editor also enables removing a specific field value for all the instances by setting it to null. In order to do this, click gui_bulkEditorSetNullButton button next to the field. After that, the field will become non-editable. The field can be unlocked by clicking the same button again.

gui invoiceBulkEdit

Example of bulkEditor use in a table:

<table id="invoiceTable"
       multiselect="true"
       width="100%">
    <actions>
        <!-- ... -->
    </actions>
    <buttonsPanel>
        <!-- ... -->
        <bulkEditor for="invoiceTable"
                    exclude="customer"/>
    </buttonsPanel>
bulkEditor attributes
  • The for attribute is required. It contains the identifier of a dataGrid, a table, or a tree; in this case, it is the invoiceTable.

  • The exclude attribute can contain a regular expression to exclude some fields explicitly from the list of attributes available for editing. For example: date|customer

    gui TableBulkEdit
  • includeProperties - defines the entity attributes to be included to bulk editor window. If set, other attributes will be ignored.

    includeProperties does not apply for dynamic attributes.

    When set declaratively, the list of properties should be comma-separated:

    <bulkEditor for="ordersTable" includeProperties="name, description"/>

    The list of properties can also be set programmatically in the screen controller:

    bulkEditor.setIncludeProperties(Arrays.asList("name", "description"));
  • loadDynamicAttributes defines whether or not the dynamic attributes of the edited entity should be displayed on the entity’s bulk editor screen. The default value is true.

  • useConfirmDialog defines whether or not the confirmation dialog should be displayed to the user before saving the changes. The default value is true.

    gui BulkEditor useConfirmDialog


3.5.2.1.5. Calendar

The Calendar component is intended to organize and display calendar events.

gui calendar 1

XML name of the component: calendar.

An example of a component definition in an XML descriptor of a screen:

<calendar id="calendar"
          captionProperty="caption"
          startDate="2016-10-01"
          endDate="2016-10-31"
          height="100%"
          width="100%"/>

The view mode is determined from the date range of the calendar, defined by the start date and the end date. The default view is the weekly view, it is used for ranges up to seven days a week. For a single-day view use the range within one date. Calendar will be shown in a monthly view when the date range is over than one week (seven days) long.

Navigation buttons to page the calendar one week forward or backward are hidden by default. To show them on a weekly view, use the navigationButtonsVisible attribute:

<calendar width="100%"
          height="100%"
          navigationButtonsVisible="true"/>
gui calendar 2

Attributes of calendar:

  • endDate - the end date for the calendar’s range.

  • endDateProperty - the name of an entity attribute that contains the end date.

  • descriptionProperty - the name of an entity attribute that contains the event description.

  • isAllDayProperty - the name of an entity attribute that determines if the event is all day long.

  • startDate - the start date for the calendar’s range.

  • startDateProperty - the name of an entity attribute that contains the start date.

  • stylenameProperty - the name of an entity attribute that contains the event style name.

  • timeFormat - time format: 12H or 24H.

    Working with Calendar events:

    To display events in the calendar cells, you can add the events directly to the Calendar object using the addEvent() method or use the CalendarEventProvider interface. An example of direct event adding:

    @Inject
    private Calendar calendar;
    
    public void generateEvent(String caption, String description, Date start, Date end, boolean isAllDay, String stylename) {
        SimpleCalendarEvent calendarEvent = new SimpleCalendarEvent();
        calendarEvent.setCaption(caption);
        calendarEvent.setDescription(description);
        calendarEvent.setStart(start);
        calendarEvent.setEnd(end);
        calendarEvent.setAllDay(isAllDay);
        calendarEvent.setStyleName(stylename);
        calendar.getEventProvider().addEvent(calendarEvent);
    }

    The removeEvent() method of CalendarEventProvider is used to remove a particular event by its index:

    CalendarEventProvider eventProvider = calendar.getEventProvider();
    List<CalendarEvent> events = new ArrayList<>(eventProvider.getEvents());
    eventProvider.removeEvent(events.get(events.size()-1));

    The removeAllEvents method, in turn, removes all available events:

    CalendarEventProvider eventProvider = calendar.getEventProvider();
    eventProvider.removeAllEvents();

    There are two data providers available: ListCalendarEventProvider (created by default) and ContainerCalendarEventProvider.

    ListCalendarEventProvider is filled by addEvent() method that gets a CalendarEvent object as a parameter:

    @Inject
    private Calendar calendar;
    
    public void addEvents() {
        ListCalendarEventProvider listCalendarEventProvider = new ListCalendarEventProvider();
        calendar.setEventProvider(listCalendarEventProvider);
        listCalendarEventProvider.addEvent(generateEvent(
                "Training", "Student training", "2016-10-17 09:00", "2016-10-17 14:00", false, "event-blue"));
        listCalendarEventProvider.addEvent(generateEvent(
                "Development", "Platform development", "2016-10-17 15:00", "2016-10-17 18:00", false, "event-red"));
        listCalendarEventProvider.addEvent(generateEvent(
                "Party", "Party with friends", "2016-10-22 13:00", "2016-10-22 18:00", false, "event-yellow"));
    }
    
    private SimpleCalendarEvent generateEvent(String caption, String description, String start, String end, Boolean allDay, String style) {
        SimpleCalendarEvent calendarEvent = new SimpleCalendarEvent();
        SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm");
        calendarEvent.setCaption(caption);
        calendarEvent.setDescription(description);
        calendarEvent.setStart(df.parse(start));
        calendarEvent.setEnd(df.parse(end));
        calendarEvent.setAllDay(allDay);
        calendarEvent.setStyleName(style);
        return calendarEvent;
    }

    ContainerCalendarEventProvider is filled with data directly from an entity fields. To be used for the ContainerCalendarEventProvider, an entity should at least have attributes for the event start date, event end date with one of the datatypes and event caption with String type.

    In the example below we assume that the CalendarEvent entity has all required attributes: eventCaption, eventDescription, eventStartDate, eventEndDate, eventStylename, and will set their names as values for calendar attributes:

    <calendar id="calendar"
              dataContainer="calendarEventsDc"
              width="100%"
              height="100%"
              startDate="2016-10-01"
              endDate="2016-10-31"
              captionProperty="eventCaption"
              descriptionProperty="eventDescription"
              startDateProperty="eventStartDate"
              endDateProperty="eventEndDate"
              stylenameProperty="eventStylename"/>

The Calendar component supports several event listeners for user interaction with its elements, such as date and week captions, date/time range selections, event dragging and event resizing. Navigation buttons used to scroll forward and backward in time are also listened by the server. Below is the list of default listeners:

  • addDateClickListener(CalendarDateClickListener listener) - adds listener for date clicks:

    calendar.addDateClickListener(
            calendarDateClickEvent ->
                    notifications.create()
                            .withCaption(String.format("Date clicked: %s", calendarDateClickEvent.getDate().toString()))
                            .show());
  • addWeekClickListener() - adds listener for week number clicks.

  • addEventClickListener() - adds listener for calendar event clicks.

  • addEventResizeListener() - adds listener for event size changing.

  • addEventMoveListener() - adds listener for event drag and drop.

  • addForwardClickListener() - adds listener for calendar forward scrolling.

  • addBackwardClickListener() - adds listener for calendar backward scrolling.

  • addRangeSelectListener() - adds listener for calendar range selection.

Calendar events can be styled with CSS. To configure a style, create the style name and set the parameters in the .scss-file. For example, let’s configure the background color of an event:

.v-calendar-event.event-green {
  background-color: #c8f4c9;
  color: #00e026;
}

Then use the setStyleName method of the event:

calendarEvent.setStyleName("event-green");

As a result, the event’s background is green:

gui calendar 3

In order to change the names of days and months in the Calendar component, use the setDayNames() and setMonthNames() methods, passing a map with the new values to them:

Map<DayOfWeek, String> days = new HashMap<>(7);
days.put(DayOfWeek.MONDAY,"Heavens and earth");
days.put(DayOfWeek.TUESDAY,"Sky");
days.put(DayOfWeek.WEDNESDAY,"Dry land");
days.put(DayOfWeek.THURSDAY,"Stars");
days.put(DayOfWeek.FRIDAY,"Fish and birds");
days.put(DayOfWeek.SATURDAY,"Animals and man");
days.put(DayOfWeek.SUNDAY,"Rest");
calendar.setDayNames(days);


3.5.2.1.6. CapsLockIndicator

This is a field that indicates if the Caps Lock is on when the user is typing a password in the PasswordField.

XML name of the component: capsLockIndicator.

gui capsLockIndicator

The capsLockOnMessage and capsLockOffMessage attributes allow you to define the messages that will be shown by the component depending on the current Caps Lock state.

Examples:

<hbox spacing="true">
    <passwordField id="passwordField"
                   capsLockIndicator="capsLockIndicator"/>
    <capsLockIndicator id="capsLockIndicator"/>
</hbox>
CapsLockIndicator capsLockIndicator = uiComponents.create(CapsLockIndicator.NAME);
capsLockIndicator.setId("capsLockIndicator");
passwordField.setCapsLockIndicator(capsLockIndicator);

The CapsLockIndicator component is designed to be used together with the PasswordField and handles the Caps Lock state only when this field is focused. When the field loses its focus, the state is changed to inactive.

Changing visibility of the CapsLockIndicator component dynamically using the visible attribute when the screen is already opened may not work as expected.

The appearance of the CapsLockIndicator component can be customized using SCSS variables with $cuba-capslockindicator-* prefix. You can change these variables in the visual editor after creating a theme extension or a custom theme.



3.5.2.1.7. CheckBox

CheckBox is a component with two states: selected or deselected.

CheckBox

Component’s XML-name: checkBox.

An example of a checkbox with a label retrieved from a localized messages pack:

<checkBox id="accessField" caption="msg://accessFieldCaption"/>

Selecting / deselecting of the checkbox changes its value: Boolean.TRUE or Boolean.FALSE. The value can be retrieved using getValue() method and set using setValue(). Submitting null using setValue() will change the value to Boolean.FALSE and uncheck the checkbox.

Changes of the checkbox value, as well as of any other components implementing the Field interface, can be tracked using a ValueChangeListener. The origin of the ValueChangeEvent can be tracked using isUserOriginated() method. For example:

@Inject
private CheckBox accessField;
@Inject
private Notifications notifications;

@Subscribe
protected void onInit(InitEvent event) {
    accessField.addValueChangeListener(valueChangeEvent -> {
        if (Boolean.TRUE.equals(valueChangeEvent.getValue())) {
            notifications.create()
                    .withCaption("set")
                    .show();
        } else {
            notifications.create()
                    .withCaption("not set")
                    .show();
        }
    });
}

The dataContainer and property attributes should be used to create a checkbox associated with data.

<data>
    <instance id="customerDc" class="com.company.sales.entity.Customer" view="_local">
        <loader/>
    </instance>
</data>
<layout>
    <checkBox dataContainer="customerDc" property="active"/>
</layout>

According to the example the screen includes the description of customerDc data container for a Customer entity with active attribute. The dataContainer attribute of the checkBox component should contain a reference to a data container; the property attribute should contain the name of an entity attribute which value should be displayed in the checkbox. The attribute should have Boolean type. If the attribute value is null the checkbox is deselected.

The appearance of the CheckBox component can be customized using SCSS variables with $cuba-checkbox-* prefix. You can change these variables in the visual editor after creating a theme extension or a custom theme.



3.5.2.1.8. CheckBoxGroup

This is a component that allows a user to select multiple values from a list of options using checkboxes.

gui CheckBoxGroup

XML name of the component: checkBoxGroup.

The CheckBoxGroup component is implemented for Web Client.

The list of component options can be specified using the setOptions(), setOptionsList(), setOptionsMap() and setOptionsEnum() methods, or using an optionsDatasource or optionsContainer attribute.

  • The simplest case of using CheckBoxGroup is to select an enumeration value for an entity attribute. For example, a Role entity has type attribute of the RoleType type, which is an enumeration. Then you can use CheckBoxGroup to display this attribute as follows, using the optionsEnum attribute:

    <checkBoxGroup optionsEnum="com.haulmont.cuba.security.entity.RoleType"
                   property="type"/>

    The setOptionsEnum() takes a class of enumeration as a parameter. The options list will consist of localized names of enum values, the value of the component will be an enum value.

    @Inject
    private CheckBoxGroup<RoleType> checkBoxGroup;
    
    @Subscribe
    protected void onInit(InitEvent event) {
        checkBoxGroup.setOptionsEnum(RoleType.class);
    }

    The same result will be achieved using the setOptions() method which enables working with all types of options:

    @Inject
    private CheckBoxGroup<RoleType> checkBoxGroup;
    
    @Subscribe
    protected void onInit(InitEvent event) {
        checkBoxGroup.setOptions(new EnumOptions<>(RoleType.class));
    }
  • setOptionsList() enables specifying programmatically a list of component options. To do this, declare a component in the XML descriptor:

    <checkBoxGroup id="checkBoxGroup"/>

    Then inject the component into the controller and specify a list of options for it:

    @Inject
    private CheckBoxGroup<Integer> checkBoxGroup;
    
    @Subscribe
    protected void onInit(InitEvent event) {
        List<Integer> list = new ArrayList<>();
        list.add(2);
        list.add(4);
        list.add(5);
        list.add(7);
        checkBoxGroup.setOptionsList(list);
    }

    The component will be as follows:

    gui CheckBoxGroup 2

    Depending on the selected option, the getValue() method of the component will return Integer values: 2, 4, 5, 7.

  • setOptionsMap() enables specifying string names and option values separately. For example, we can set the following options map for the checkBoxGroup component injected in the controller:

    @Inject
    private CheckBoxGroup<Integer> checkBoxGroup;
    
    @Subscribe
    protected void onInit(InitEvent event) {
        Map<String, Integer> map = new LinkedHashMap<>();
        map.put("two", 2);
        map.put("four", 4);
        map.put("five", 5);
        map.put("seven", 7);
        checkBoxGroup.setOptionsMap(map);
    }

    The component will be as follows:

    gui CheckBoxGroup 3

    Depending on the selected option, the getValue() method of the component will return Integer values: 2, 4, 5, 7, and not the strings that are displayed on the screen.

  • The component can take a list of options from a data container. For this purpose, the optionsContainer attribute is used. For example:

    <data>
        <collection id="employeesCt" class="com.company.demo.entity.Employee" view="_minimal">
            <loader>
                <query><![CDATA[select e from demo_Employee e]]></query>
            </loader>
        </collection>
    </data>
    <layout>
        <checkBoxGroup optionsContainer="employeesCt"/>
    </layout>

    In this case, the checkBoxGroup component will display instance names of the Employee entity, located in the employeesCt data container, and its getValue() method will return the selected entity instance.

    gui CheckBoxGroup 4

    With the help of captionProperty attribute entity attribute to be used instead of an instance name for string option names can be defined.

    Programmatically, you can define the options container using the setOptions() method of CheckBoxGroup interface:

    @Inject
    private CheckBoxGroup<Employee> checkBoxGroup;
    @Inject
    private CollectionContainer<Employee> employeesCt;
    
    @Subscribe
    protected void onInit(InitEvent event) {
        checkBoxGroup.setOptions(new ContainerOptions<>(employeesCt));
    }

The orientation attribute defines the orientation of group elements. By default, elements are arranged vertically. The horizontal value sets the horizontal orientation.



3.5.2.1.9. ColorPicker

ColorPicker is a field that allows a user to preview and select a color. Component returns a hexadecimal (HEX) value of the color as a string.

gui color picker

An example of a color picker with a caption retrieved from the localized messages pack:

<colorPicker id="colorPicker" caption="msg://colorPickerCaption"/>

The figure below shows an example of the color picker with the popup closed.

gui color picker mini

To create a color picker connected to data, use dataContainer and property attributes.

<data>
    <collection id="carsDc" class="com.company.sales.entity.Car" view="_local">
        <loader>
            <query>
                <![CDATA[select e from sales_Car e]]>
            </query>
        </loader>
    </collection>
</data>
<layout>
    <colorPicker id="colorPicker" dataContainer="carsDc" property="color"/>
</layout>

Attributes of сolorPicker:

  • buttonCaption - caption of the component button.

  • defaultCaptionEnabled - if set to true and buttonCaption is not set, displays HEX value as a button caption.

  • historyVisible - determines the visibility of history of recently picked colors in the popup window.

You can determine visibility of the popup tabs using the following attributes:

  • rgbVisible - determines the visibility of the RGB tab.

  • hsvVisible - determines the visibility of the HSV tab.

  • swatchesVisible - determines the visibility of the swatches tab.

By default, only the RGB tab is visible.

Also, if you want to redefine the labels in popup, you can use caption attributes:

  • popupCaption - caption of the popup window.

  • confirmButtonCaption - caption of the confirm button.

  • cancelButtonCaption - caption of the cancel button.

  • swatchesTabCaption - swatches tab caption.

  • lookupAllCaption - caption of lookup item for all colors.

  • lookupRedCaption - caption of lookup item for red color.

  • lookupGreenCaption - caption of lookup item for green color.

  • lookupBlueCaption - caption of lookup item for blue color.

getValue() method of the component returns a string, containing a HEX code of the selected color.



3.5.2.1.10. CurrencyField

CurrencyField is a subtype of a text field designed for currency value input. It has a currency symbol inside the field and is aligned to the right by default.

gui currencyField

XML-name of the component: currencyField.

CurrencyField is implemented for Web Client only.

Basically, CurrencyField repeats the functionality of TextField. You can manually set a datatype for the field, except that only numeric datatypes inheriting NumericDatatype class are supported. If the datatype is unparseable, an exception will be thrown.

CurrencyField can be bound to a data container using the dataContainer and property attributes:

<currencyField currency="$"
               dataContainer="orderDc"
               property="amount"/>

currencyField attributes:

  • currency - a text that will be a content of the currency label.

    <currencyField currency="USD"/>
  • currencyLabelPosition - sets the position of currency label inside the field:

    • LEFT - to the left from the text input component,

    • RIGHT - to the right from the text input component (default value).

  • showCurrencyLabel - defines whether the currency label should be displayed.



3.5.2.1.11. DataGrid

DataGrid, similarly to the Table component, is designed to display and sort tabular data, and provides means to manipulate rows and columns with greater performance due to lazy loading of data while scrolling.

gui dataGrid 1

XML name of the component: dataGrid.

An example of component definition in an XML-descriptor of a screen:

<data>
    <collection id="ordersDc" class="com.company.sales.entity.Order" view="order-with-customer">
        <loader id="ordersDl">
            <query>
                <![CDATA[select e from sales_Order e order by e.date]]>
            </query>
        </loader>
    </collection>
</data>
<layout>
    <dataGrid id="ordersDataGrid" dataContainer="ordersDc" height="100%" width="100%">
        <columns>
            <column id="date" property="date"/>
            <column id="customer" property="customer.name"/>
            <column id="amount" property="amount"/>
        </columns>
    </dataGrid>
</layout>

In the example above the id attribute is a column identifier, and the property is the name of the entity attribute from the data container that populates the column with data.

dataGrid elements:

  • columns - mandatory element that defines the DataGrid columns set. Element columns has the following attributes:

    • includeAll – load all the attributes from the view that is defined in dataContainer or datasource.

      In the example below, we will show all the attributes from the view used in the customersDc. If the view contains system properties, they will be shown too.

      <dataGrid id="dataGrid"
                width="100%"
                height="100%"
                dataContainer="customersDc">
          <columns includeAll="true"/>
      </dataGrid>

      If the view of the entity contains a reference attribute, this attribute will be displayed according to its instance name. If you want to show a specific attribute, it must be defined in the view as well as in the column element:

      <columns includeAll="true">
          <column id="address.street"/>
      </columns>

      If no view is specified, includeAll attribute will load all the attributes from a given entity and its ancestors.

    • exclude – comma-separated list of attributes that should not be loaded to the DataGrid.

      In the example below, we will show all the attributes excluding name and order:

      <dataGrid id="dataGrid"
                width="100%"
                height="100%"
                dataContainer="customersDc">
          <columns includeAll="true"
                   exclude="name, order"/>
      </dataGrid>

    Each column is described in the nested column element with the following attributes:

    • id - an optional attribute with the column identifier. If not set, the string with the property value will be used as the column identifier. In this case setting the property value is mandatory, otherwise the GuiDevelopmentException exception will be thrown. The id attribute is still mandatory for the columns created in the screen controller.

    • property - contains the entity attribute’s name. Can be either an attribute of the entity from the data source / data container or a linked entity – object graph traversal is indicated with a dot. For example:

      <columns>
          <column id="date" property="date"/>
          <column id="customer" property="customer"/>
          <column id="customerName" property="customer.name"/>
          <column id="customerCountry" property="customer.address.country"/>
      </columns>
    • caption - an optional attribute containing the column caption. If not specified, a localized attribute name will be displayed.

    • expandRatio - sets the column width ratio. By default, all columns have equal width (i.e. expandRatio = 1). If another value is set for at least one column, all implicit values are ignored, and only set values are considered.

    • collapsible - defines whether a user can hide or show columns using the sidebar menu in the top right of DataGrid. The default value is true

    • collapsed - an optional attribute; hides the column by default when set to true. The default value is false.

    • collapsingToggleCaption - sets the column’s caption in the sidebar menu. By default its value is null, in this case the caption remains the same as the column’s caption.

      gui dataGrid 2
    • resizable - defines whether a user can change the column’s size.

    • sortable - an optional attribute to disable sorting of the column. Takes effect if the whole DataGrid has sortable attribute set to true (which is by default).

    • width - an optional attribute controlling default column width. May contain only numeric values in pixels.

    • minimumWidth - sets the minimal column width in pixels.

    • maximumWidth - sets the maximal column width in pixels.

    The column element may contain a nested formatter element that allows you to represent the attribute value in a format different from the standard for this DataType:

    <column id="date" property="date">
        <formatter class="com.haulmont.cuba.gui.components.formatters.DateFormatter"
                   format="yyyy-MM-dd HH:mm:ss"
                   useUserTimezone="true"/>
    </column>
  • actions - optional element to define actions for DataGrid. Besides custom actions, the standard actions from the ListActionType enumeration are also supported: create, edit, remove, refresh, add, exclude.

  • buttonsPanel - creates a ButtonsPanel container with action buttons above the DataGrid component.

  • rowsCount - optional element that creates a RowsCount component for the DataGrid. RowsCount enables pagination of data, the page size is set by limitation of records in the data loader with the help of CollectionLoader.setMaxResults() method from the screen controller. Another way to do this is to use a universal Filter component bound with the same data container as the DataGrid.

The RowsCount component can also display the total number of records returned by current data request without loading these records. When a user clicks the "?" button, it calls the com.haulmont.cuba.core.global.DataManager#getCount method that passes to the database a request with the same parameters as current but with COUNT(*) aggregation function instead of getting results. The returned number is displayed in place of "?" symbol.

dataGrid attributes:

  • columnResizeMode - sets the mode of columns resizing by user. Two modes are supported:

    • ANIMATED - the columns size follows the mouse when dragging (default mode).

    • SIMPLE - the columns size is changed after the dragging is finished.

    The column size changes can be tracked with ColumnResizeListener. The origin of the column size changes event can be tracked using isUserOriginated() method.

  • columnsCollapsingAllowed - defines whether a user can hide columns in the sidebar menu. Displayed columns are checked in the menu. When a column name is checked/unchecked, the value of collapsed attribute of each column is updated. When set to false, the collapsed attribute of any column cannot be set to true.

    The column collapsing changes can be tracked with ColumnCollapsingChangeListener. The origin of the column collapsing event can be tracked using isUserOriginated() method.

  • contextMenuEnabled - enables turning on and off the context menu. Default value is true.

    The right mouse clicks on the DataGrid can be tracked with ContextClickListener.

  • editorBuffered - sets the buffered editor mode. The default mode is buffered (true).

  • editorCancelCaption - sets the caption on the cancel button in the DataGrid editor.

  • editorCrossFieldValidate - enables cross field validation in the inline editor. Default value is true.

  • editorSaveCaption - sets the caption on the save button in the DataGrid inline editor.

  • frozenColumnCount - sets the number of fixed DataGrid columns. The 0 value means that no columns will be fixed except the predefined column with checkboxes for multiple choice if the multiselect mode is used. The -1 value makes even multiselect column not fixed.

  • headerVisible - defines if the DataGrid header is visible. The default value is true.

  • reorderingAllowed - defines whether a user can change the columns order by dragging them with a mouse. The default value is true.

    The column order changes can be tracked with ColumnReorderListener. The origin of the order change event can be tracked using isUserOriginated() method.

  • selectionMode - sets the rows selection mode. There are 4 predefined selection modes:

    • SINGLE - single record selection.

    • MULTI - multiple selection as in any table.

    • MULTI_CHECK - multiple selection using the embedded column with checkboxes.

    • NONE - selection is disabled.

      Rows selection events can be tracked by SelectionListener. The origin of the selection event can be tracked using isUserOriginated() method.

      gui dataGrid 3
  • sortable - enables or disables the DataGrid sorting. The default value is true. When the sorting is enabled, the click on the column name will display the sorting icon to the right of che column caption. Sorting of any specific column can be disabled by this column’s sortable attribute.

    The DataGrid sorting events can be tracked by SortListener. The origin of the sorting event can be tracked using isUserOriginated() method.

  • textSelectionEnabled - enables or disables text selection in the DataGrid cells. The default value is false.

Methods of the DataGrid interface:

  • getColumns() - returns the current set of DataGrid columns in their current display order.

  • getSelected(), getSingleSelected() - return instances of the entities corresponding to the selected rows of the table. A collection can be obtained by invoking getSelected(). If nothing is selected, the application returns an empty set. If SelectionMode.SINGLE is set, it is more convenient to use getSingleSelected() method returning one selected entity or null, if nothing is selected.

  • getVisibleColumns() - returns the current set of visible DataGrid columns in their current display order.

  • scrollTo() - method allows you to scroll the DataGrid to the specified row. It takes an entity instance identifying the row as a parameter. Besides the entity instance, an overloaded method can take a ScrollDestination parameter with the following possible values:

    • ANY - scroll as little as possible to show the required record.

    • START - scroll to place the required record in the beginning of the DataGrid visible area.

    • MIDDLE - scroll to place the required record in the centre of the DataGrid visible area.

    • END - scroll to place the required record in the end of the DataGrid visible area.

  • scrollToStart() and scrollToEnd() - scroll the DataGrid to the top and to the end respectively.

  • addCellStyleProvider() - adds style provider for the DataGrid cells.

  • addRowStyleProvider() - adds style provider for the DataGrid rows.

  • setEnterPressAction() - method allows you to define an action executed when Enter is pressed. If such action is not defined, the table will attempt to find an appropriate one in the list of its actions in the following order:

    • The action defined by the setItemClickAction() method.

    • The action assigned to the Enter key by the shortcut property.

    • The edit action.

    • The view action.

    If such action is found, and has enabled = true property, the action is executed.

  • setItemClickAction() - method allows you to define an action that will be performed when a table row is double-clicked. If such action is not defined, the table will attempt to find an appropriate one in the list of its actions in the following order:

    • The action assigned to the Enter key by the shortcut property.

    • The edit action.

    • The view action.

    If such action is found, and has enabled = true property, the action is executed.

    Item click events can be tracked with ItemClickListener.

  • sort() - sorts the data for the specified column in the sort direction chosen from 2 values of the SortDirection enum:

    • ASCENDING - ascending (e.g. A-Z, 1..9) sort order.

    • DESCENDING - descending (e.g. Z-A, 9..1) sort order.

Usage of description providers:

  • setDescriptionProvider() method is used to generate optional descriptions (tooltips) for the cells of individual DataGrid columns. The description may contain HTML markup.

    @Inject
    private DataGrid<Customer> customersDataGrid;
    
    @Subscribe
    protected void onInit(InitEvent event) {
        customersDataGrid.getColumnNN("age").setDescriptionProvider(customer ->
                        getPropertyCaption(customer, "age") +
                                customer.getAge(),
                ContentMode.HTML);
    
        customersDataGrid.getColumnNN("active").setDescriptionProvider(customer ->
                        getPropertyCaption(customer, "active") +
                                getMessage(customer.getActive() ? "trueString" : "falseString"),
                ContentMode.HTML);
    
        customersDataGrid.getColumnNN("grade").setDescriptionProvider(customer ->
                        getPropertyCaption(customer, "grade") +
                                messages.getMessage(customer.getGrade()),
                ContentMode.HTML);
    }
    gui dataGrid 11
  • setRowDescriptionProvider() method is used to generate optional descriptions (tooltips) for DataGrid rows. If a column description provider is also set, the row description generated by provider is used for cells for which the cell description provider returns null.

    customersDataGrid.setRowDescriptionProvider(Instance::getInstanceName);
    gui dataGrid 10

Usage of the DetailsGenerator interface:

The DetailsGenerator interface allows you to create a custom component to display the details of a particular row using the setDetailsGenerator() method:

@Inject
private DataGrid<Order> ordersDataGrid;
@Inject
private UiComponents uiComponents;

@Install(to = "ordersDataGrid", subject = "detailsGenerator")
protected Component ordersDataGridDetailsGenerator(Order order) {
    VBoxLayout mainLayout = uiComponents.create(VBoxLayout.NAME);
    mainLayout.setWidth("100%");
    mainLayout.setMargin(true);

    HBoxLayout headerBox = uiComponents.create(HBoxLayout.NAME);
    headerBox.setWidth("100%");

    Label infoLabel = uiComponents.create(Label.NAME);
    infoLabel.setHtmlEnabled(true);
    infoLabel.setStyleName("h1");
    infoLabel.setValue("Order info:");

    Component closeButton = createCloseButton(order);
    headerBox.add(infoLabel);
    headerBox.add(closeButton);
    headerBox.expand(infoLabel);

    Component content = getContent(order);

    mainLayout.add(headerBox);
    mainLayout.add(content);
    mainLayout.expand(content);

    return mainLayout;
}

private Component createCloseButton(Order entity) {
    Button closeButton = uiComponents.create(Button.class);
    // ... (1)
    return closeButton;
}

private Component getContent(Order entity) {
    Label<String> content = uiComponents.create(Label.TYPE_STRING);
    content.setHtmlEnabled(true);
    StringBuilder sb = new StringBuilder();
    // ... (2)
    content.setValue(sb.toString());
    return content;
}
1 – See the full code of the createCloseButton method in the DataGridDetailsGeneratorSample class.
2 – See the full code of the getContent method in the DataGridDetailsGeneratorSample class.

Result:

gui dataGrid 15

Usage of DataGrid inline editor:

The DataGrid component has an API for inline editing of records in the cells. When an item is being edited, the inline edit UI with default save and cancel buttons is displayed.

Methods of inline editor API:

  • getEditedItem() - returns the item that is currently being edited.

  • isEditorActive() - returns whether an item is currently being edited in the editor.

  • editItem(Object itemId) (Deprecated) - opens the editor interface for the provided item id. Scrolls the Grid to bring the item to view if it is not already visible.

  • edit(Entity item) - opens the editor interface for the provided item. Scrolls the Grid to bring the item to view if it is not already visible.

DataGrid inline editor can take into account entity constraints (cross field validation). If there are validation errors, DataGrid will show an error message. To enable/disable validation or get current state use the following methods:

  • setEditorCrossFieldValidate(boolean validate) - enables or disables the cross field validation in the inline editor. The default value is true.

  • isEditorCrossFieldValidate() - return true if the inline editor validates cross field rules.

You can add and remove listeners to the editor using the following methods:

  • addEditorOpenListener(), removeEditorOpenListener() - DataGrid editor open listener.

    This listener is triggered by a double click on the DataGrid area that instantiates the inline editor and enables to get the fields of the edited row. This enables to update some fields depending on other fields' values without closing the editor.

    For example:

    customersTable.addEditorOpenListener(editorOpenEvent -> {
        Map<String, Field> fieldMap = editorOpenEvent.getFields();
        Field active = fieldMap.get("active");
        Field grade = fieldMap.get("grade");
    
        ValueChangeListener listener = e ->
                active.setValue(true);
        grade.addValueChangeListener(listener);
    });
  • addEditorCloseListener(), removeEditorCloseListener() - DataGrid editor close listener.

  • addEditorPreCommitListener(), removeEditorPreCommitListener() - DataGrid editor pre commit listener.

  • addEditorPostCommitListener(), removeEditorPostCommitListener() - DataGrid editor post commit listener.

The changes are committed to the data source or data container only. The logic to save these changes in the database should be added separately.

The editor field can be customized with the help of EditorFieldGenerationContext class. Apply the setEditFieldGenerator() method to a column in order to set a custom component for editing this column:

@Inject
private DataGrid<Order> ordersDataGrid;
@Inject
private UiComponents uiComponents;

@Subscribe
protected void onInit(InitEvent event) {
    ordersDataGrid.getColumnNN("amount").setEditFieldGenerator(orderEditorFieldGenerationContext -> {
        LookupField<BigDecimal> lookupField = uiComponents.create(LookupField.NAME);
        lookupField.setValueSource((ValueSource<BigDecimal>) orderEditorFieldGenerationContext
                .getValueSourceProvider().getValueSource("amount"));
        lookupField.setOptionsList(Arrays.asList(BigDecimal.ZERO, BigDecimal.ONE, BigDecimal.TEN));

        return lookupField;
    });
}

The result:

gui dataGrid 14

Usage of the ColumnGenerator interface:

DataGrid enables adding generated columns with the help of the methods:

  • addGeneratedColumn(String columnId, ColumnGenerator generator)

  • addGeneratedColumn(String columnId, ColumnGenerator generator, int index)

ColumnGenerator is a special interface that defines the generated, or calculated, column:

  • value of each column’s row,

  • the type of value - common for the whole column.

Below is an example of generating a column that displays users' login in the upper case:

@Subscribe
protected void onInit(InitEvent event) {
    DataGrid.Column column = usersGrid.addGeneratedColumn("loginUpperCase", new DataGrid.ColumnGenerator<User, String>(){
        @Override
        public String getValue(DataGrid.ColumnGeneratorEvent<User> event){
            return event.getItem().getLogin().toUpperCase();
        }

        @Override
        public Class<String> getType(){
            return String.class;
        }
    }, 1);
    column.setCaption("Login Upper Case");
}

The result:

gui dataGrid 7

ColumnGeneratorEvent, passed in the getValue method, contains information on the entity, displayed in the current DataGrid row, and the column identifier.

By default, the generated column is added to the end of the table. There are two possible ways to manage the column’s position: either using an index in the code or adding a column in advance in the XML descriptor and pass its id to the addGeneratedColumn method.

Usage of renderers:

The way the data is displayed in columns can be customized by means of renderers. For example, to display icons as pictures in the cells, the path to an icon can be used together with the ImageRenderer class:

@Subscribe
protected void onInit(InitEvent event) {
    DataGrid.Column avatar = usersGrid.addGeneratedColumn("userAvatar", new DataGrid.ColumnGenerator<User, String>() {
        @Override
        public String getValue(DataGrid.ColumnGeneratorEvent<User> event) {
            return "icons/user.png";
        }

        @Override
        public Class<String> getType() {
            return String.class;
        }
    }, 0);
    avatar.setCaption("Avatar");
    avatar.setRenderer(usersGrid.createRenderer(DataGrid.ImageRenderer.class));
}

The result:

gui dataGrid 8

The WebComponentRenderer interface allows you to display components of different web components types in the DataGrid cells. This interface is implemented only in the Web Module. Below is an example of creating a column with the LookupField component:

@Inject
private DataGrid<User> usersGrid;
@Inject
private UiComponents uiComponents;
@Inject
private Configuration configuration;
@Inject
private Messages messages;

@Subscribe
protected void onInit(InitEvent event) {
    Map<String, Locale> locales = configuration.getConfig(GlobalConfig.class).getAvailableLocales();
    Map<String, String> options = new TreeMap<>();
    for (Map.Entry<String, Locale> entry : locales.entrySet()) {
        options.put(entry.getKey(), messages.getTools().localeToString(entry.getValue()));
    }

    DataGrid.Column column = usersGrid.addGeneratedColumn("language",
            new DataGrid.ColumnGenerator<User, Component>() {
                @Override
                public Component getValue(DataGrid.ColumnGeneratorEvent<User> event) {
                    LookupField<String> component = uiComponents.create(LookupField.NAME);
                    component.setOptionsMap(options);
                    component.setWidth("100%");

                    User user = event.getItem();
                    component.setValue(user.getLanguage());

                    component.addValueChangeListener(e -> user.setLanguage(e.getValue()));

                    return component;
                }

                @Override
                public Class<Component> getType() {
                    return Component.class;
                }
            });

    column.setRenderer(new WebComponentRenderer());
}

The result:

gui dataGrid 13

When the field type does not match the data type that can be processed by a renderer, one can create a Function to match data types of the model and the view. For example, to display a boolean value as an icon, it would be handy to use the HtmlRenderer to display HTML layout and implement the logic to convert a boolean value to the layout for icons' display.

@Inject
private DataGrid<User> usersGrid;

@Subscribe
protected void onInit(InitEvent event) {

    DataGrid.Column<User> hasEmail = usersGrid.addGeneratedColumn("hasEmail", new DataGrid.ColumnGenerator<User, Boolean>() {
        @Override
        public Boolean getValue(DataGrid.ColumnGeneratorEvent<User> event) {
            return StringUtils.isNotEmpty(event.getItem().getEmail());
        }

        @Override
        public Class<Boolean> getType() {
            return Boolean.class;
        }
    });

    hasEmail.setCaption("Has Email");
    hasEmail.setRenderer(
        usersGrid.createRenderer(DataGrid.HtmlRenderer.class),
        (Function<Boolean, String>) hasEmailValue -> {
            return BooleanUtils.isTrue(hasEmailValue)
                    ? FontAwesome.CHECK_SQUARE_O.getHtml()
                    : FontAwesome.SQUARE_O.getHtml();
        });
}

The result:

gui dataGrid 9

The renderers can be created in two ways:

  • passing a renderer interface to the fabric method of the DataGrid interface. Suits for GUI and Web modules.

  • directly creating a renderer implementation for the corresponding module:

    dataGrid.createRenderer(DataGrid.ImageRenderer.class) → new WebImageRenderer()

    For the moment this way is suitable only for the Web module.

The list of renderers supported by the Platform:

  • IconRenderer - a renderer that represents CubaIcon.

  • TextRenderer - displays plain text.

  • HtmlRenderer - displays HTML layout.

  • ProgressBarRenderer - displays double values between 0 and 1 as a ProgressBar component.

  • DateRenderer - displays dates in the defined format.

  • NumberRenderer - displays numbers in the defined format.

  • ButtonRenderer - displays string values as a button caption.

  • ImageRenderer - uses the path to an image to display the image.

  • CheckBoxRenderer - displays boolean values as a checkbox icon.

Header and Footer:

HeaderRow and FooterRow interfaces are used to represent header and footer cells respectively. They can be a merged cell for multiple columns.

The following methods of DataGrid allow to create and manage the DataGrid header and footer:

  • appendHeaderRow(), appendFooterRow() - adds a new row at the bottom of the header/footer section.

  • prependHeaderRow(), prependFooterRow() - adds a new row at the top of the header/footer section.

  • addHeaderRowAt(), addFooterRowAt() - inserts a new row at the given position to the header/footer section. Shifts the row currently at that position and any subsequent rows down incrementing their indices.

  • removeHeaderRow(), removeFooterRow() - removes the given row from the header/footer section.

  • getHeaderRowCount(), getFooterRowCount() - gets the row count for the header/footer section.

  • setDefaultHeaderRow() - sets the default row of the header. The default row is a special header row providing a user interface for sorting columns.

HeaderCell and FooterCell interfaces provide means of customization of static DataGrid cells:

  • setStyleName() - sets a custom style name for this cell.

  • getCellType() - returns the type of content stored in this cell. There are 3 types of DataGridStaticCellType enumeration available:

    • TEXT

    • HTML

    • COMPONENT

  • getComponent(), getHtml(), getText() - returns the content displayed in this cell depending on its type.

Below is an example of DataGrid the header that contains merged cells, and the footer displaying calculated values.

<dataGrid id="dataGrid" datasource="countryGrowthDs" width="100%">
    <columns>
        <column property="country"/>
        <column property="year2017"/>
        <column property="year2018"/>
    </columns>
</dataGrid>
@Inject
private DataGrid<CountryGrowth> dataGrid;
@Inject
private UserSessionSource userSessionSource;
@Inject
private Messages messages;
@Inject
private CollectionContainer<CountryGrowth> countryGrowthsDc;

private DecimalFormat percentFormat;

@Subscribe
protected void onBeforeShow(BeforeShowEvent event) {
    initPercentFormat();
    initHeader();
    initFooter();
    initRenderers();
}

private DecimalFormat initPercentFormat() {
    percentFormat = (DecimalFormat) NumberFormat.getPercentInstance(userSessionSource.getLocale());
    percentFormat.setMultiplier(1);
    percentFormat.setMaximumFractionDigits(2);
    return percentFormat;
}

private void initRenderers() {
    dataGrid.getColumnNN("year2017").setRenderer(new WebNumberRenderer(percentFormat));
    dataGrid.getColumnNN("year2018").setRenderer(new WebNumberRenderer(percentFormat));
}

private void initHeader() {
    DataGrid.HeaderRow headerRow = dataGrid.prependHeaderRow();
    DataGrid.HeaderCell headerCell = headerRow.join("year2017", "year2018");
    headerCell.setText("GDP growth");
    headerCell.setStyleName("center-bold");
}

private void initFooter() {
    DataGrid.FooterRow footerRow = dataGrid.appendFooterRow();
    footerRow.getCell("country").setHtml("<strong>" + messages.getMainMessage("average") + "</strong>");
    footerRow.getCell("year2017").setText(percentFormat.format(getAverage("year2017")));
    footerRow.getCell("year2018").setText(percentFormat.format(getAverage("year2018")));
}

private double getAverage(String propertyId) {
    double average = 0.0;
    List<CountryGrowth> items = countryGrowthsDc.getItems();
    for (CountryGrowth countryGrowth : items) {
        Double value = countryGrowth.getValue(propertyId);
        average += value != null ? value : 0.0;
    }
    return average / items.size();
}
gui dataGrid 12

The appearance of the DataGrid component can be customized using SCSS variables with $cuba-datagrid-* prefix. You can change these variables in the visual editor after creating a theme extension or a custom theme.


Attributes of dataGrid

align - caption - captionAsHtml - colspan - columnResizeMode - columnsCollapsingAllowed - contextHelpText - contextHelpTextHtmlEnabled - contextMenuEnabled - css - dataContainer - datasource - description - descriptionAsHtml - editorBuffered - editorCancelCaption - editorCrossFieldValidate - editorEnabled - editorSaveCaption - enable - box.expandRatio - frozenColumnCount - headerVisible - height - icon - id - reorderingAllowed - responsive - rowspan - selectionMode - settingsEnabled - sortable - stylename - tabIndex - textSelectionEnabled - visible - width

Elements of dataGrid

actions - buttonsPanel - columns - rowsCount

Attributes of columns

includeAll - exclude

Attributes of column

caption - collapsed - collapsible - collapsingToggleCaption - editable - expandRatio - id - maximumWidth - minimumWidth - property - resizable - sortable - width

Elements of column

formatter

API

addGeneratedColumn - applySettings - createRenderer - edit - saveSettings - getColumns - setDescriptionProvider - addCellStyleProvider - setConverter - setDetailsGenerator - setEditorCrossFieldValidate - setEnterPressAction - setItemClickAction - setRenderer - setRowDescriptionProvider - addRowStyleProvider - sort

Listeners of dataGrid

ColumnCollapsingChangeListener - ColumnReorderListener - ColumnResizeListener - ContextClickListener - EditorCloseListener - EditorOpenListener - EditorPostCommitListener - EditorPreCommitListener - ItemClickListener - SelectionListener - SortListener

3.5.2.1.12. DateField

DateField is a field to display and enter date and time. It is an input field, inside which there is a button with a drop-down calendar. To the right, there is a time field.

gui dateFieldSimple

XML name of the component: dateField.

  • To create a date field associated with data, you should use the dataContainer and property attributes:

    <data>
        <instance id="orderDc"
                  class="com.company.sales.entity.Order"
                  view="_local">
            <loader/>
        </instance>
    </data>
    <layout>
        <dateField dataContainer="orderDc"
                   property="date"/>
    </layout>

    In the example above, the screen has the orderDc data container for the Order entity, which has the date property. The reference to the data container is specified in the dataContainer attribute of the dateField component; the name of the entity attribute which value should be displayed in the field is specified in the property attribute.

  • If the field is associated with an entity attribute, it will automatically take the appropriate form:

    • If the attribute has the java.sql.Date type or the @Temporal(TemporalType.DATE) annotation is specified, the time field will not be displayed. The date format is defined by the date datatype and is specified in the main localized message pack in the dateFormat key.

    • Otherwise, the time field with hours and minutes will be displayed. The time format is defined by the time datatype and is specified in the main localized message pack in the timeFormat key.

  • If the field is not connected to an entity attribute (i.e. the data container and attribute name are not set), you can set the data type using the datatype attribute. DateField uses the following data types:

    • date

    • dateTime

    • localDate

    • localDateTime

    • offsetDateTime

  • You can change the date and time format using the dateFormat attribute. An attribute value can be either a format string itself or a key in a message pack (if the value starts with msg://).

    The format is defined by rules of the SimpleDateFormat class (http://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html). If there are no H or h characters in the format, the time field will not be displayed.

    <dateField dateFormat="MM/yy" caption="msg://monthOnlyDateField"/>
    gui dateField format

    DateField is primarily intended for quick input by filling placeholders from keyboard. Therefore the component supports only formats with digits and separators. Complex formats with textual representation of weekdays or months will not work.

  • You can specify available dates by using rangeStart and rangeEnd attributes. If a range is set, all dates outside the range will be disabled. You can set range dates in the "yyyy-MM-dd" format in XML or programmatically by using corresponding setters.

    <dateField id="dateField" rangeStart="2016-08-15" rangeEnd="2016-08-19"/>
    gui datefield month range
  • Changes of the DateField value, as well as of any other components implementing the Field interface, can be tracked using a ValueChangeListener. The origin of the ValueChangeEvent can be tracked using isUserOriginated() method.

  • Date and time accuracy can be defined using a resolution attribute. An attribute value should match the DateField.Resolution enumeration − SEC, MIN, HOUR, DAY, MONTH, YEAR. Default is MIN, i.e., to within a minute.

    If resolution="DAY" and dateFormat is not specified, the format will be taken from one specified in the main message pack with the dateFormat key.

    If resolution="MIN" and dateFormat is not specified, the format will be taken from one specified in the main message pack with the dateTimeFormat key. Below is a field definition for entering a date up to within a month.

    <dateField resolution="MONTH" caption="msg://monthOnlyDateField"/>
gui dateField resolution
  • DateField can perform timestamp value conversions between server and user time zones if the user’s time zone is set by setTimeZone() method. The time zone is assigned automatically from the current user session when the component is bound to an entity attribute of the timestamp type. If the component is not bound to such attribute, you can call setTimeZone() in the screen controller to make the DateField perform required conversions..

  • Today’s date in the calendar is determined against current timestamp in a user’s web browser, which depends on the OS time zone settings. User’s time zone doesn’t affect this behaviour.

  • In Web Client with a Halo-based theme, you can set predefined borderless style name to the DateField component to remove borders and background from the field. This can be done either in the XML descriptor or in the screen controller:

    <dateField id="dateField"
               stylename="borderless"/>

    When setting a style programmatically, select the HaloTheme class constant with the DATEFIELD_ prefix:

    dateField.setStyleName(HaloTheme.DATEFIELD_BORDERLESS);


3.5.2.1.13. DatePicker

DatePicker is a field to display and choose a date. It has the same view as the drop-down calendar in DateField.

gui datepicker mini

XML name of the component: datePicker.

The DatePicker component is implemented for Web Client.

  • To create a date picker associated with data, you should use the dataContainer/datasource and property attributes:

    <data>
        <instance id="orderDc"
                  class="com.company.sales.entity.Order"
                  view="_local">
            <loader/>
        </instance>
    </data>
    <layout>
        <datePicker id="datePicker"
                    dataContainer="orderDc"
                    property="date"/>
    </layout>

    In the example above, the screen has the orderDc data container for the Order entity, which has the date property. The reference to the data container is specified in the dataContainer attribute of the datePicker component; the name of the entity attribute which value should be displayed in the field is specified in the property attribute.

  • You can specify available dates to select by using rangeStart and rangeEnd attributes. If you set them, all the dates that are outside the range will be disabled.

    <datePicker id="datePicker" rangeStart="2016-08-15" rangeEnd="2016-08-19"/>
    gui datepicker month range
  • Date accuracy can be defined using a resolution attribute. An attribute value should match the DatePicker.Resolution enumeration − DAY, MONTH, YEAR. Default resolution is DAY.

    <datePicker id="datePicker" resolution="MONTH"/>
    gui datepicker month resolution
    <datePicker id="datePicker" resolution="YEAR"/>
    gui datepicker year resolution
  • Today’s date in the calendar is determined against current timestamp in a user’s web browser, which depends on the OS time zone settings. User’s time zone doesn’t affect this behaviour.



3.5.2.1.14. Embedded (Deprecated)

Starting from the version 6.8 of the Platform the Embedded component is deprecated. Use the Image component for displaying images or the BrowserFrame component for embedded web pages.

Embedded component is intended for displaying images and embedding arbitrary web pages into the application screens.

XML name of the component: embedded

Below is an example of using the component to display an image from a file located in FileStorage.

  • Declare the component in an XML screen descriptor:

    <groupBox caption="Embedded" spacing="true"
              height="250px" width="250px" expand="embedded">
        <embedded id="embedded" width="100%"
                  align="MIDDLE_CENTER"/>
    </groupBox>
  • In a screen controller, inject the component itself and the FileStorageService interface. In init() method, get the FileDescriptor passed from the calling code, load the corresponding file in a byte array, create a ByteArrayInputStream for it, and pass the stream to the setSource() method of the component:

    @Inject
    private Embedded embedded;
    @Inject
    private FileStorageService fileStorageService;
    
    @Override
    public void init(Map<String, Object> params) {
        FileDescriptor imageFile = (FileDescriptor) params.get("imageFile");
        byte[] bytes = null;
        if (imageFile != null) {
            try {
                bytes = fileStorageService.loadFile(imageFile);
            } catch (FileStorageException e) {
                showNotification("Unable to load image file", NotificationType.HUMANIZED);
            }
        }
        if (bytes != null) {
            embedded.setSource(imageFile.getName(), new ByteArrayInputStream(bytes));
            embedded.setType(Embedded.Type.IMAGE);
        } else {
            embedded.setVisible(false);
        }
    }

The Embedded component supports several different content types, which are rendered differently in HTML. You can set the content type with the setType() method. Supported types:

  • OBJECT - allows embedding certain file types inside HTML <object> and <embed> elements.

  • IMAGE - embeds an image inside a HTML <img> element.

  • BROWSER - embeds a browser frame inside a HTML <iframe> element.

In Web Client, the component enables displaying of files located inside VAADIN folder. You can set the resource path relative to the application root, for example:

<embedded id="embedded"
          relativeSrc="VAADIN/themes/halo/my-logo.png"/>

or

embedded.setRelativeSource("VAADIN/themes/halo/my-logo.png")

You can also define a resource files directory in the cuba.web.resourcesRoot application property and specify the name of a file inside this directory with the prefix for the value: file:// , url:// , or theme://:

<embedded id="embedded"
          src="file://my-logo.png"/>

or

embedded.setSource("theme://branding/app-icon-menu.png");

In order to display an external web page, pass its URL to the component:

try {
    embedded.setSource(new URL("http://www.cuba-platform.com"));
} catch (MalformedURLException e) {
    throw new RuntimeException(e);
}


3.5.2.1.15. FieldGroup

FieldGroup is intended for the joint display and editing of multiple entity attributes.

gui fieldGroup

XML-name of the component: fieldGroup

FieldGroup works only in screens based on legacy API. The similar functionality for the current API is provided by the Form component.

Below is an example of defining a group of fields in an XML screen descriptor:

<dsContext>
    <datasource id="orderDs"
                class="com.sample.sales.entity.Order"
                view="order-with-customer">
    </datasource>
</dsContext>
<layout>
    <fieldGroup id="orderFieldGroup" datasource="orderDs" width="250px">
        <field property="date"/>
        <field property="customer"/>
        <field property="amount"/>
    </fieldGroup>
</layout>

In the example above, dsContext defines an orderDs data source, which contains a single instance of the Order entity. The data source is specified in the datasource attribute of the fieldGroup component. field elements refer to the entity attributes that need to be displayed in the component.

Elements of fieldGroup:

  • column – optional element that allows you to position fields in multiple columns. For this purpose, field elements should be placed not immediately within fieldGroup, but within a column. For example:

    <fieldGroup id="orderFieldGroup" datasource="orderDs" width="100%">
        <column width="250px">
            <field property="num"/>
            <field property="date"/>
            <field property="amount"/>
        </column>
        <column width="400px">
            <field property="customer"/>
            <field property="info"/>
        </column>
    </fieldGroup>

    In this case, fields will be arranged in two columns; the first column will contain all fields with the width of 250px, the second one with the width of 400px.

    Attributes of column:

    • width – specifies the field width of a column. By default, fields have the width of 200px. In this attribute, the width can be specified both in pixels and in percentage of the total horizontal width of the column.

    • flex – a number, which indicates the degree of horizontal change in the overall size of the column relative to other columns as a result of changing the entire width of fieldGroup. For example, you can specify flex=1 for a column, and flex=3 for another one.

    • id – an optional column identifier, which allows you to refer to it in case of screen extension.

  • field – the main component element. It defines one field of the component.

    Custom fields can be included in the field element as inline XML definition:

    <fieldGroup>
        <field id="demo">
            <lookupField id="demoField" datasource="userDs" property="group"/>
        </field>
    </fieldGroup>

    Attributes of field:

    • id – required attribute, if property is not set; otherwise it takes the same value as property by default. The id attribute should contain an arbitrary unique identifier either of a field with the property attribute set, or a programmatically defined field. In the latter case, the field should have the attribute custom="true" as well (see below).

    • property - required attribute, if id is not set; it should contain an entity attribute name, which is displayed in the field, for data binding.

    • caption − allows you to specify a field caption. If not specified, an entity attribute localized name will be displayed.

    • inputPrompt - if the inputPrompt attribute is available for the component used for this field, you can set its value directly for the field.

    • visible − allows you to hide the field together with the caption.

    • datasource − allows you to specify a data source for the field, other than specified for the entire fieldGroup component. Thus, attributes of different entities can be displayed in a field group.

    • optionsDatasource specifies a name of a data source, used to create a list of options. You can specify this attribute for a field connected to a reference entity attribute. By default, the selection of a related entity is made through a lookup screen. If optionsDatasource is specified, you can select the related entity from a drop-down list of options. Actually, specifying optionsDatasource will lead to the fact that LookupPickerField will be used in the field instead of PickerField.

    • width − allows you to specify the field width excluding caption. By default, the field width will be 200px. The width can be specified both in pixels and in percentage of the total horizontal width of the column. To specify the width of all fields simultaneously, you can use the width attribute of the column element described above.

    • custom – if set to true, it means that a field identifier does not refer to an entity attribute, and a component, which is in the field, will be set programmatically using setComponent() method of FieldGroup (see below).

    • the generator attribute is used for declarative creation of custom fields: you can specify the name of the method that returns a custom component for this field:

      <fieldGroup datasource="productDs">
          <column width="250px">
              <field property="description" generator="generateDescriptionField"/>
          </column>
      </fieldGroup>
      public Component generateDescriptionField(Datasource datasource, String fieldId) {
          TextArea textArea = uiComponents.create(TextArea.NAME);
          textArea.setRows(5);
          textArea.setDatasource(datasource, fieldId);
          return textArea;
      }
    • linkScreen - contains the identifier of the screen that is opened by clicking the link, enabled in the link attribute.

    • linkScreenOpenType - sets the screen opening mode (THIS_TAB, NEW_TAB or DIALOG).

    • linkInvoke - contains the controller method to be invoked instead of opening the screen.

    The following attributes of field can be applied depending on the type of the entity attribute displayed in the field:

    • If you specify a value of the mask attribute for a text entity attribute, MaskedField with an appropriate mask will be used instead of TextField. In this case, you can also specify the valueMode attribute.

    • If you specify a value of the rows attribute for a text entity attribute, TextArea with the appropriate number of rows will be used instead of TextField. In this case, you can also specify the cols attribute.

    • For a text entity attribute, you can specify the maxLength attribute similarly to one described for TextField.

    • For an entity attribute of the date or dateTime type, you can specify the dateFormat and resolution for the parameterization of the DateField component used in the field.

    • For an entity attribute of the time type, you can specify the showSeconds attribute for the parameterization of the TimeField component used in the field.

Attributes of fieldGroup:

  • The border attribute can be set either to hidden or visible. Default is hidden. If set to visible, the fieldGroup component is highlighted with a border. In the web implementation of the component, displaying a border is done by adding the cuba-fieldgroup-border CSS class.

  • captionAlignment attribute defines the position of captions relative to the fields in within the FieldGroup. Two options are available: LEFT and TOP.

  • fieldFactoryBean: declarative fields defined in the XML-descriptor are created with the FieldGroupFieldFactory interface. In order to override this factory, use this attribute with the name of your custom FieldGroupFieldFactory implementation.

    For the FieldGroup created programmatically, use the setFieldFactory() method.

Methods of the FieldGroup interface:

  • addField enables adding fields to the FieldGroup at runtime. As a parameter it takes a FieldConfig instance, you can also define the position of the new field by adding colIndex and rowIndex parameters.

  • bind() method applied to the field after setDatasource() triggers the creation of field components.

  • createField() is used to create a FieldGroup element implementing FieldConfig interface:

    fieldGroup.addField(fieldGroup.createField("newField"));
  • getComponent() returns a visual component, which is located in a field with the specified identifier. This may be required for additional component parameterization, which is not available through XML attributes of field described above.

    To obtain a reference to a field component in a screen controller, you can use injection instead of the explicit invocation of getFieldNN("id").getComponentNN(). To do this, use the @Named annotation and provide an identifier of fieldGroup and a field identifier after a dot.

    For example, in a field selecting a related entity, you can add an action to open an instance and remove the field cleaning action as follows:

    <fieldGroup id="orderFieldGroup" datasource="orderDs">
        <field property="date"/>
        <field property="customer"/>
        <field property="amount"/>
    </fieldGroup>
    @Named("orderFieldGroup.customer")
    protected PickerField customerField;
    
    @Override
    public void init(Map<String, Object> params) {
        customerField.addOpenAction();
        customerField.removeAction(customerField.getAction(PickerField.ClearAction.NAME));
    }

    To use getComponent() or to inject field components, you need to know which component type is located in the field. The table below shows the correspondence between entity attribute types and components created for them:

    Entity attribute type Additional conditions Field component type

    Related Entity

    optionsDatasource is specified

    LookupPickerField

    PickerField

    Enumeration (enum)

    LookupField

    string

    mask is specified

    MaskedField

    rows is specified

    TextArea

    TextField

    boolean

    CheckBox

    date, dateTime

    DateField

    time

    TimeField

    int, long, double, decimal

    mask is specified

    MaskedField

    TextField

    UUID

    MaskedField with hex mask

  • removeField() enables removing fields at runtime by id.

  • setComponent() method is used to set your own field view. Can be used together with the custom="true" attribute of the field element or with the field created programmatically by the createField() method (see above). When used with custom="true", the datasource and the property should be set up manually.

    The FieldConfig instance can be obtained with getField() or getFieldNN() method, then the setComponent() method is called:

    @Inject
    protected FieldGroup fieldGroup;
    @Inject
    protected UiComponents uiComponents;
    @Inject
    private Datasource<User> userDs;
    
    @Override
    public void init(Map<String, Object> params) {
        PasswordField passwordField = uiComponents.create(PasswordField.NAME);
        passwordField.setDatasource(userDs, "password");
        fieldGroup.getFieldNN("password").setComponent(passwordField);
    }


3.5.2.1.16. FileMultiUploadField

The FileMultiUploadField component allows a user to upload files to the server. The component is a button; when it is clicked, a standard OS file picker window is shown, where the user can select multiple files for upload.

gui multipleUpload

XML name of the component: multiUpload.

Below is an example of using FileMultiUploadField.

  • Declare the component in an XML screen descriptor:

    <multiUpload id="multiUploadField" caption="Upload Many"/>
  • In the screen controller, inject the component itself, the FileUploadingAPI and DataManager interfaces.

    @Inject
    private FileMultiUploadField multiUploadField;
    @Inject
    private FileUploadingAPI fileUploadingAPI;
    @Inject
    private Notifications notifications;
    @Inject
    private DataManager dataManager;
    
    @Subscribe
    protected void onInit(InitEvent event) { (1)
    
        multiUploadField.addQueueUploadCompleteListener(queueUploadCompleteEvent -> { (2)
            for (Map.Entry<UUID, String> entry : multiUploadField.getUploadsMap().entrySet()) { (3)
                UUID fileId = entry.getKey();
                String fileName = entry.getValue();
                FileDescriptor fd = fileUploadingAPI.getFileDescriptor(fileId, fileName); (4)
                try {
                    fileUploadingAPI.putFileIntoStorage(fileId, fd); (5)
                } catch (FileStorageException e) {
                    throw new RuntimeException("Error saving file to FileStorage", e);
                }
                dataManager.commit(fd); (6)
            }
            notifications.create()
                    .withCaption("Uploaded files: " + multiUploadField.getUploadsMap().values())
                    .show();
            multiUploadField.clearUploads(); (7)
        });
    
        multiUploadField.addFileUploadErrorListener(queueFileUploadErrorEvent -> {
            notifications.create()
                    .withCaption("File upload error")
                    .show();
        });
    }
    1 In the onInit() method, add listeners which will react on successful uploads and errors.
    2 The component uploads all selected files to the temporary storage of the client tier and invokes the listener added by the addQueueUploadCompleteListener() method.
    3 In this listener, the FileMultiUploadField.getUploadsMap() method is invoked to obtain a map of temporary storage file identifiers to file names.
    4 Then, corresponding FileDescriptor objects are created by calling FileUploadingAPI.getFileDescriptor() for each map entry. com.haulmont.cuba.core.entity.FileDescriptor (do not confuse with java.io.FileDescriptor) is a persistent entity, which uniquely identifies an uploaded file and then is used to download the file from the system.
    5 FileUploadingAPI.putFileIntoStorage() method is used to move the uploaded file from the temporary client storage to FileStorage. Parameters of this method are temporary storage file identifier and the FileDescriptor object.
    6 After uploading the file to FileStorage, the FileDescriptor instance is saved in a database by invoking DataManager.commit(). The saved instance returned by this method can be set to an attribute of an entity related to this file. Here, FileDescriptor is simply stored in the database. The file will be available through the Administration > External Files screen.
    7 After processing, the list of files should be cleared by calling the clearUploads() method in order to prepare for further uploads.

Below is the list of listeners available to track the upload process:

  • FileUploadErrorListener,

  • FileUploadStartListener,

  • FileUploadFinishListener,

  • QueueUploadCompleteListener.

Maximum upload size is determined by the cuba.maxUploadSizeMb application property and is 20MB by default. If a user selects a file of a larger size, a corresponding message will be displayed, and the upload will be interrupted.

multiUpload attributes:

  • The accept XML attribute (and the corresponding setAccept() method) can be used to set the file type mask in the file selection dialog. Users still be able to change the mask to "All files" and upload arbitrary files.

    The value of the attribute should be a comma-separated list of masks. For example: *.jpg,*.png.

  • The fileSizeLimit XML attribute (and the corresponding setFileSizeLimit() method) can be used to set maximum allowed file size in bytes. This limit defines a maximum size for each file.

    <multiUpload id="multiUploadField" fileSizeLimit="200000"/>
  • The permittedExtensions XML attribute (and the corresponding setPermittedExtensions() method) sets the white list of permitted file extensions.

    The value of the attribute should be a set of string values, where each string is a permitted extension with leading dot. For example:

    uploadField.setPermittedExtensions(Sets.newHashSet(".png", ".jpg"));
  • The dropZone XML attribute allows you to specify a BoxLayout to be used as a target for drag-and-dropping files from outside of the browser. If the container style is not set, the selected container is highlighted when a user drags files over the container, otherwise the dropZone is not visible.

See Loading and Displaying Images for more complex example of working with uploaded files.



3.5.2.1.17. FileUploadField

The FileUploadField component allows a user to upload files to the server. The component can contain a caption, a link to uploaded file, and two buttons: for uploading and for clearing the selected file. When the upload button is clicked, a standard OS file picker window is shown, where the user can select a file. In order to upload multiple files, use FileMultiUploadField.

gui upload 7.0

XML name of the component: upload.

For entity attributes of FileDescriptor type, the component can be used inside FieldGroup with datasource attribute, inside Form with dataContainer attribute, or independently. If the component is bound to any data component, the uploaded file is immediately stored in file storage and the corresponding FileDescriptor instance is saved to the database.

<upload fileStoragePutMode="IMMEDIATE"
        dataContainer="personDc"
        property="photo"/>

You can also control the saving of file and FileDescriptor programmatically:

  • Declare the component in an XML screen descriptor:

    <upload id="uploadField"
            fileStoragePutMode="MANUAL"/>
  • In the screen controller, inject the component itself, the FileUploadingAPI and DataManager interfaces. Then subscribe to the events of successful uploads or error:

    @Inject
    private FileUploadField uploadField;
    @Inject
    private FileUploadingAPI fileUploadingAPI;
    @Inject
    private DataManager dataManager;
    @Inject
    private Notifications notifications;
    
    @Subscribe("uploadField")
    public void onUploadFieldFileUploadSucceed(FileUploadField.FileUploadSucceedEvent event) {
        File file = fileUploadingAPI.getFile(uploadField.getFileId()); (1)
        if (file != null) {
            notifications.create()
                    .withCaption("File is uploaded to temporary storage at " + file.getAbsolutePath())
                    .show();
        }
    
        FileDescriptor fd = uploadField.getFileDescriptor(); (2)
        try {
            fileUploadingAPI.putFileIntoStorage(uploadField.getFileId(), fd); (3)
        } catch (FileStorageException e) {
            throw new RuntimeException("Error saving file to FileStorage", e);
        }
        dataManager.commit(fd); (4)
        notifications.create()
                .withCaption("Uploaded file: " + uploadField.getFileName())
                .show();
    }
    
    @Subscribe("uploadField")
    public void onUploadFieldFileUploadError(UploadField.FileUploadErrorEvent event) {
        notifications.create()
                .withCaption("File upload error")
                .show();
    }
1 Here you can get the file uploaded to the temporary storage if you need it.
2 Normally, you would want to save the file to the file storage of the middle tier.
3 Save file to FileStorage.
4 Save file descriptor to database.

The component will upload the file to the temporary storage of the client tier and invoke the FileUploadSucceedEvent listener. In this listener, a FileDescriptor object is requested from the component. com.haulmont.cuba.core.entity.FileDescriptor is a persistent entity, which uniquely identifies an uploaded file and is used to download the file from the system.

FileUploadingAPI.putFileIntoStorage() method is used to move the uploaded file from the temporary client storage to FileStorage. Parameters of this method are temporary storage file identifier and the FileDescriptor object. Both of these parameters are provided by FileUploadField.

After uploading the file to FileStorage, the FileDescriptor instance is saved in the database by invoking DataManager.commit(). The saved instance returned by this method can be set to an attribute of an entity related to this file. Here, FileDescriptor is simply stored in the database. The file will be available through the Administration > External Files screen.

The FileUploadErrorEvent listener will be invoked if an error occurs when uploading a file to the temporary storage of the client tier.

Below is the list of events available to subscribe to track the upload process:

  • AfterValueClearEvent,

  • BeforeValueClearEvent,

  • FileUploadErrorEvent,

  • FileUploadFinishEvent

  • FileUploadStartEvent,

  • FileUploadSucceedEvent,

  • ValueChangeEvent.

fileUploadField attributes:

  • fileStoragePutMode - defines how the file and the corresponding FileDescriptor are stored.

    • In the IMMEDIATE mode it is done right after uploading file to the temporary storage of the client tier.

    • In the MANUAL mode, you should do it programmatically in a FileUploadSucceedListener.

    The IMMEDIATE mode is selected by default when FileUploadField is used inside FieldGroup. Otherwise, the default mode is MANUAL.

  • uploadButtonCaption, uploadButtonIcon and uploadButtonDescription XML attributes allow you to set the properties of the upload button.

  • showFileName - controls whether the name of uploaded file is displayed next to upload button. It is false by default.

  • showClearButton - controls whether the clear button is visible. It is false by default.

  • clearButtonCaption, clearButtonIcon and clearButtonDescription XML attributes allow you to set the properties of the clear button if it is visible.

  • The accept XML attribute (and the corresponding setAccept() method) can be used to set the file type mask in the file selection dialog. Users still be able to change the mask to "All files" and upload arbitrary files.

    The value of the attribute should be a comma-separated list of masks. For example: *.jpg,*.png.

  • Maximum upload size is determined by the cuba.maxUploadSizeMb application property and is 20MB by default. If a user selects a file of a larger size, a corresponding message will be displayed, and the upload will be interrupted.

  • The fileSizeLimit XML attribute (and the corresponding setFileSizeLimit() method) can be used to set maximum allowed file size specified in bytes.

    <upload id="uploadField" fileSizeLimit="2000"/>
  • The permittedExtensions XML attribute (and the corresponding setPermittedExtensions() method) sets the white list of permitted file extensions.

    The value of the attribute should be a comma-separated list of extensions with leading dots. For example:

    uploadField.setPermittedExtensions(Sets.newHashSet(".png", ".jpg"));
  • dropZone - allows you to specify a BoxLayout to be used as a target for drag-and-dropping files from outside of the browser. The dropZone can cover the whole layout of a dialog window. The selected container is highlighted when a user drags a file over the container, otherwise it is not visible.

    <layout spacing="true"
            width="100%">
        <vbox id="dropZone"
              height="AUTO"
              spacing="true">
            <textField id="textField"
                       caption="Title"
                       width="100%"/>
            <textArea id="textArea"
                      caption="Description"
                      width="100%"
                      rows="5"/>
            <checkBox caption="Is reference document"
                      width="100%"/>
            <upload id="upload"
                    dropZone="dropZone"
                    showClearButton="true"
                    showFileName="true"/>
        </vbox>
        <hbox spacing="true">
            <button caption="mainMsg://actions.Apply"/>
            <button caption="mainMsg://actions.Cancel"/>
        </hbox>
    </layout>
    gui dropZone

    To make a dropZone static and display it permanently, assign the predefined dropzone-container style to its container. In this case the container should be empty with only the label component inside:

    <layout spacing="true"
            width="100%">
        <textField id="textField"
                   caption="Title"
                   width="100%"/>
        <checkBox caption="Is reference document"
                  width="100%"/>
        <upload id="upload"
                dropZone="dropZone"
                showClearButton="true"
                showFileName="true"/>
        <vbox id="dropZone"
              height="150px"
              spacing="true"
              stylename="dropzone-container">
            <label stylename="dropzone-description"
                   value="Drop file here"
                   align="MIDDLE_CENTER"/>
        </vbox>
        <hbox spacing="true">
            <button caption="mainMsg://actions.Apply"/>
            <button caption="mainMsg://actions.Cancel"/>
        </hbox>
    </layout>
    gui dropZone static
  • pasteZone allows you to specify a container to be used for handling paste shortcuts when a text input field nested in this container is focused. This feature is supported by Chromium-based browsers.

    <upload id="uploadField"
            pasteZone="vboxId"
            showClearButton="true"
            showFileName="true"/>

See Loading and Displaying Images for more complex example of working with uploaded files.



3.5.2.1.18. Filter

In this section:

The Filter is a versatile tool for filtering lists of entities extracted from a database to display in a tabular form. The component enables quick data filtering by arbitrary conditions, as well as creating filters for repeated use.

Filter should be connected to a CollectionContainer with a loader or a CollectionDatasource containing a JPQL query. Its logic is based on the modification of the query in accordance with the criteria provided by the user. Thus, filtering is done at the database level when the SQL query is executed, and only selected data is loaded to the Middleware and Client tiers.

Using a Filter

A typical filter is shown below:

gui filter descr

By default, the component is in quick filter mode. This means that a user can add a set of conditions for a one-off data search. After the screen is closed, the conditions will disappear.

To create a quick filter, click Add search condition link. The condition selection screen will be displayed:

gui filter conditions

Possible condition types are described below:

  • Attributes – attributes of this entity and related entities. Only persistent attributes are displayed. They should also either be explicitly set in the property element of the filter XML descriptor, or comply with the rules specified in the properties element.

  • Custom conditions – conditions specified by developer in the custom elements of the filter XML descriptor.

  • Create new…​ – enables creating a new arbitrary JPQL condition. This option is only available to users having the specific cuba.gui.filter.customConditions permission.

Selected conditions are displayed at the top of the filter panel. The gui_filter_remove_condition icon will appear next to each condition field, allowing them to be removed from the set.

Quick filters can be saved for further re-use. In order to save a quick filter, click the filter settings icon, select Save/Save as and provide a new filter name in the popup dialog:

gui filter name

After that, the filter will be saved and will appear in the drop-down menu of the Search button.

The Reset filter menu lets you reset all currently applied search conditions.

gui filter reset

The filter settings popup button provides the list of options for filter management:

  • Save – save changes to the current filter.

  • Save with values – save changes to the current filter using the values in parameter editors as filter default values.

  • Save as – save the filter under a new name.

  • Edit – open the filter editor (see below).

  • Make default – make the filter default for this screen. The filter will be automatically displayed on the filter panel when the screen is opened.

  • Remove – delete the current filter.

  • Pin applied – use the results of the last search for sequential data filtering (see Applying Filters Sequentially).

  • Save as search folder – create a search folder based on the current filter.

  • Save as application folder – create an application folder based on the current filter. This option is available to users having the specific cuba.gui.appFolder.global permission only.

The Edit option opens the filter editor, allowing advanced configuration of the current filter:

gui filter editor

Filter name should be provided in the Name field. This name will be displayed in available filters list for the current screen.

Filter can be made global (i.e., available to all users) using the Available to all users checkbox and global default using the Global default checkbox. These operations require a specific permission called CUBA > Filter > Create/modify global filters. If the filter is marked as global default then it will be automatically selected when users open the screen. Users can set their own default filters using the Default for me checkbox. This setting overrides the global default one.

The filter conditions are contained in the tree. They can be added using the Add button, swapped using gui_filter_cond_down/gui_filter_cond_up or removed using the Remove button.

AND or OR grouping conditions can be added with the help of the corresponding buttons. All top level conditions (i.e., without explicit grouping) are joined with AND.

Selecting a condition in the tree opens the list of its properties in the right part of the editor.

The conditions can be made hidden or required by means of corresponding checkboxes. The hidden condition parameter is invisible to the user, so it should be provided when the filter is being edited.

Width property enables selecting the width of the parameter field on the filter panel for the current condition. By default, conditions on the filter panel are displayed in three columns. The field width equals to the number of columns it will occupy (1, 2 or 3).

Default parameter value for the current condition can be selected in the Default value field.

A custom caption for filter condition can be provided in the Caption field.

Operation field enables selecting the condition operator. The list of available operators depends on the attribute type.

If an entity has an attribute of the DateTime type with no @IgnoreUserTimeZone annotation, the the user’s time zone will be respected in filters for this attribute by default. As for the Date type, you can define whether the user’s time zone should be respected by using the special Use time zone flag in the custom condition editor.

Filter Component

XML name of the component: filter.

An example of component declaration in XML screen descriptor is shown below:

<data readOnly="true">
    <collection id="carsDc" class="com.haulmont.sample.core.entity.Car" view="carBrowse">
        <loader id="carsDl" maxResults="50">
            <query>
                <![CDATA[select e from sample_Car e order by e.createTs]]>
            </query>
        </loader>
    </collection>
</data>
<layout expand="carsTable" spacing="true">
    <filter id="filter" applyTo="carsTable" dataLoader="carsDl">
        <properties include=".*"/>
    </filter>
    <table id="carsTable" width="100%" dataContainer="carsDc">
        <columns>
            <column id="vin"/>
            <column id="colour"/>
            <column id="model"/>
        </columns>
        <rowsCount/>
    </table>
</layout>

In the example above, a data container is defined in the data layer of the screen. The container selects Car entity instances using JPQL query. The data loader which provides data to be filtered is specified in the filter component’s loader attribute. Data is displayed using the Table component, which is connected to the same data container.

filter may contain nested elements. They describe conditions available for user selection in Add Condition dialog:

  • properties – multiple entity attributes can be made available for selection. This element has the following attributes:

    • include – required attribute. It contains a regular expression, which should match an entity attribute name.

    • exclude – contains a regular expression. If an attribute matches the expression, it will be excluded from previously included (using include).

    • excludeProperties – contains a comma-separated list of property paths that should be excluded from filtering. As opposed to exclude, it supports traversing the entity graphs, for example: customer.name.

    • excludeRecursively - defines if an attribute from excludeProperties should be excluded recursively for the whole object graph. If true, an attribute and all its nested attributes with the same name will be excluded.

      For example:

      <filter id="filter"
              applyTo="ordersTable"
              dataLoader="ordersDl">
          <properties include=".*"
                      exclude="(amount)|(id)"
                      excludeProperties="version,createTs,createdBy,updateTs,updatedBy,deleteTs,deletedBy"
                      excludeRecursively="true"/>
      </filter>

      To exclude properties programmatically, use the setPropertiesFilterPredicate() method of the Filter component:

      filter.setPropertiesFilterPredicate(metaPropertyPath ->
              !metaPropertyPath.getMetaProperty().getName().equals("createTs"));

    The following entity attributes are ignored when properties element is used:

    • Not accessible due to security permissions.

    • Collections (@OneToMany, @ManyToMany).

    • Non-persistent attributes.

    • Attributes that do not have localized names.

    • Attributes annotated with @SystemLevel.

    • Attributes of type byte[].

    • The version attribute.

  • property – explicitly includes an entity attribute by name. This element has the following attributes:

    • name – required attribute, containing the name of entity attribute to be included. It can be a path (using ".") in the entity graph. For example:

      <filter id="transactionsFilter" dataLoader="transactionsDl" applyTo="table">
          <properties include=".*" exclude="(masterTransaction)|(authCode)"/>
          <property name="creditCard.maskedPan" caption="msg://EmbeddedCreditCard.maskedPan"/>
          <property name="creditCard.startDate" caption="msg://EmbeddedCreditCard.startDate"/>
      </filter>
    • caption – localized entity attribute name displayed in filter conditions. Generally it is a string with the msg:// prefix in accordance with MessageTools.loadString() rules.

      If the name attribute is specified as an entity graph path (using ".") , the caption attribute is required.

    • paramWhere − specifies the JPQL expression which is used to select the list of condition parameter values if the parameter is a related entity. The {E} placeholder should be used in the expression instead of the alias of the entity being selected.

      For example, let’s assume that Car has a reference to Model. Then possible condition parameter values list can be limited to Audi models only:

      <filter id="carsFilter" dataLoader="carsDl">
          <property name="model" paramWhere="{E}.manufacturer = 'Audi'"/>
      </filter>

      Screen parameters, session attributes and screen components including those showing other parameters can be used in JPQL expression. Query parameters specification rules are described in Dependencies Between Data Components and CollectionDatasourceImpl Queries.

      An example of session and screen parameters usage is shown below:

      {E}.createdBy = :session$userLogin and {E}.name like :param$groupName

      With the paramWhere clause, you can introduce dependencies between parameters. For example, let’s assume that Manufacturer is a separate entity. That is Car has a reference to Model which in turn has a reference to Manufacturer. Then you may want to create two conditions for the Cars filter: first to select a Manufacturer and second to select a Model. To restrict the list of models by previously selected manufacturer, add a parameter to the paramWhere expression:

      {E}.manufacturer.id = :component$filter.model_manufacturer90062

      The parameter references a component which displays Manufacturer parameter. You can see the name of the component showing condition parameter by opening context menu on a condition table row in the filter editor:

      gui filter component name
    • paramView − specifies a view, which will be used to load the list of condition parameter values if the parameter is a related entity. For example, _local. If view is not specified, _minimal view will be used.

  • custom is an element defining an arbitrary condition. The element content should be a JPQL expression (JPQL Macros can be used), which will be added to the data container query’s where clause. The {E} placeholder should be used in the expression instead of the alias of the entity being selected. The condition can only have one parameter denoted by "?" if used.

    A value of custom condition can contain special characters, for example "%" or "_" for "like" operator. If you want to escape these characters, add escape '<char>' to your condition, for example:

    {E}.name like ? escape '\'

    Then if you use foo\% as a value of the condition parameter, the search will interpret "%" as a character in your name and not as a special character.

    An example of a filter with arbitrary conditions is shown below:

    <filter id="carsFilter" dataLoader="carsDl">
        <properties include=".*"/>
        <custom name="vin" paramClass="java.lang.String" caption="msg://vin">
          {E}.vin like ?
        </custom>
        <custom name="colour" paramClass="com.company.sample.entity.Colour" caption="msg://colour"
                inExpr="true">
          ({E}.colour.id in (?))
        </custom>
        <custom name="repair" paramClass="java.lang.String" caption="msg://repair"
                join="join {E}.repairs cr">
          cr.description like ?
        </custom>
        <custom name="updateTs" caption="msg://updateTs">
          @between({E}.updateTs, now-1, now+1, day)
        </custom>
    </filter>

    custom conditions are displayed in the Custom conditions section of the Add condition dialog:

    gui filter custom

    Attributes of custom:

    • name − required attribute, condition name.

    • caption − required attribute, localized condition name. Generally it is a string with the msg:// prefix in accordance with MessageTools.loadString() rules.

    • paramClass − Java class of the condition parameter. If the parameter is not specified, this attribute is optional.

    • inExpr − should be set to true, if the JPQL expression contains in (?) conditions. In this case user will be able to enter several condition parameter values.

    • join − optional attribute. It specifies a string, which will be added to the data container query from section. This can be required to create a complex condition based on an attribute of a related collection. join or left join statements should be included into the attribute value.

      For example, let’s assume that the Car entity has a repairs attribute, which is a related entity Repair instances collection. Then the following condition can be created to filter Car by Repair entity’s description attribute:

      <filter id="carsFilter" dataLoader="carsDl">
          <custom name="repair"
                  caption="msg://repair"
                  paramClass="java.lang.String"
                  join="join {E}.repairs cr">
              cr.description like ?
          </custom>
      </filter>

      If the condition above is used, the original data container query

      select c from sample_Car c order by c.createTs

      will be transformed into the following one:

      select c from sample_Car c join c.repairs cr
      where (cr.description like ?)
      order by c.createTs

      Also, you can create a custom condition with an unrelated entity and use this entity later in the where section of the condition. In this case, you should use ", " instead of join or left join statements in the join attribute value.

      Below is an example of a custom condition to find cars that were assigned to drivers after the specified date:

      <filter id="carsFilter"
              dataLoader="carsLoader"
              applyTo="carsTable">
          <custom name="carsFilter"
                  caption="carsFilter"
                  paramClass="java.util.Date"
                  join=", ref$DriverAllocation da">
              da.car = {E} and da.createTs >= ?
          </custom>
      </filter>
    • paramWhere − specifies a JPQL expression used to select the list of condition parameter values if the parameter is a related entity. See the description of the property element’s attribute of the same name.

    • paramView − specifies a view, which will be used when a list of condition parameter values are loaded if the parameter is a related entity. See the description of the property element’s attribute of the same name.

filter attributes:

  • editable – if the attribute value is false, the Edit option is disabled.

  • applyImmediately – specifies when the filter is applied. If the attribute value is false, the filter will be applied only after the Search button is clicked. If the attribute value is true, filter is applied immediately after changing the filter conditions. General cases when a filter is applied immediately:

    • After the parameter field’s value changing;

    • After changing condition operation;

    • After removing a condition from the filter;

    • After the Show rows field changing;

    • When you click on the OK button in the filter editor dialog;

    • After clearing all values.

    In the immediate mode, the Refresh button is used instead of Search.

    The applyImmediately attribute takes precedence over the cuba.gui.genericFilterApplyImmediately application property.

  • manualApplyRequired − defines when the filter will be applied. If the attribute value is false, the filter (default or empty) will be applied when the screen is opened. It means that the data container will be refreshed and linked components (e.g. Table) will display data. If the value is true, the filter will be applied only after the Search button is clicked.

    This attribute takes precedence over the cuba.gui.genericFilterManualApplyRequired application property.

  • useMaxResults − limits the page size of entity instances loaded into the data container. It is set to true by default.

    If the attribute value is false, the filter will not show the Show rows field. The number of records in the data container (and displayed in the table accordingly) will be limited only by the MaxFetchUI parameter of the entity statistics, which is set to 10000 by default.

    If the attribute is not specified or is true, the Show rows field will be displayed only if the user has specific cuba.gui.filter.maxResults permission. If the cuba.gui.filter.maxResults permission is not granted, the filter will force selecting only the first N rows without user to be able to disable it or specify another N. N is defined by FetchUI, DefaultFetchUI parameters. They are obtained from the entity statistics mechanism.

    A filter shown below has the following parameters: useMaxResults="true", the cuba.gui.filter.maxResults permission is denied, and cuba.gui.filter.maxResults DefaultFetchUI = 2.

gui filter useMaxRezult
  • textMaxResults - enables using the text field instead of the drop-down list as the Show rows field. false by default.

  • folderActionsEnabled − if it is set to false, the following filter actions will be hidden: Save as Search Folder, Save as Application Folder. By default, the attribute value is true, and Save as Search Folder, Save as Application Folder are available.

  • applyTo − optional attribute, contains the identifier of a component associated with the filter. It is used when access to related component presentations is required. For example, when saving the filter as a search folder or as an application folder, the presentation that will be applied when browsing this folder can be specified.

    gui filter apply to
  • caption - enables setting a custom caption for the filter panel.

  • columnsCount - defines the number of columns for conditions on the filter panel. Default value is 3.

  • defaultMode - defines the filter default mode. Possible values are generic and fts. When fts value is set then the filter will be opened in the full text search mode (if the entity is indexed). The default value is generic.

  • modeSwitchVisible - defines the visibility of the checkbox that switches the filter to the full text search mode. If full text search is unavailable then the checkbox will be invisible despite of the defined value. Possible values are true and false (true by default).

Methods of Filter interface:

  • setBorderVisible() - defines if the filter border should be displayed. The default value is true.

Listeners of Filter:

  • ExpandedStateChangeListener - enables tracking the expanded state changes.

  • FilterEntityChangeListener - is triggered when a filter is selected for the first time on a component initialization or later from the list of saved filters.

The appearance of the Filter component can be customized using SCSS variables with $cuba-filter-* prefix. You can change these variables in the visual editor after creating a theme extension or a custom theme.



User Permissions

  • To create/change/delete global (available to all users) filters, user must have the cuba.gui.filter.global permission.

  • To create/change custom conditions user must have a cuba.gui.filter.customConditions permission.

  • To change the maximum number of rows per table page using the Show rows field, user must have the cuba.gui.filter.maxResults permission. See also the useMaxResults filter attribute.

For specific permissions configuration information, see Security Subsystem.

External Filter Control Parameters

  • Screen invocation parameters

    It is possible to specify a filter and its parameters which should be applied when the screen is opened. For this purpose, the filter should be created in advance, stored in the database, and a corresponding record in the SEC_FILTER table should have a value in the CODE field. Screen invocation parameters are set in the web-menu.xml configuration file.

    In order to store the filter in the database, the insert script for the filter should be added to the 30.create-db.sql script of the entity. To simplify the script creation, find the filter in the Entity Inspector section of the Administration menu, in the filter’s context menu choose System Information, click Script for insert button and copy the script.

    Then you can adjust the screen to use the filter by default. To specify a filter code, pass to the screen a parameter with the same name as filter component identifier in this screen. Parameter value should be the code of the filter.

    To set filter parameter values, pass to the screen parameters with the names equal to parameter names and their values in string format.

    An example of main menu item descriptor is shown below. It sets a filter with the FilterByVIN code to the carsFilter component of the sample_Car.browse screen which it opens. It also sets TMA value to the component$carsFilter.vin79216 condition:

    <item id="sample_Car.browse">
        <param name="carsFilter" value="FilterByVIN"/>
        <param name="component$carsFilter.vin79216" value="TMA"/>
    </item>

    It should be noted that a filter with a defined CODE field has some specifics:

    • It cannot be edited by users.

    • Name of this filter can be displayed in several languages. To achieve this, specify a string with key equal to the filter code in the application main message pack.

Applying Filters Sequentially

If the cuba.allowQueryFromSelected application property is enabled, the last applied filter and the current filtered results can be pinned via the component’s user interface. After that another filter or other parameters of the current filter can be selected and applied to the currently selected records.

This approach helps to achieve two aims:

  • Decompose complex filters, which may lead to better performance as well.

  • Apply filters to the records selected using application or search folders.

Take the following steps to use sequential filters. First, choose and apply one of the filters. Next click the filter settings button and select Pin applied. The filter will be pinned at the top of the filter panel. Then another filter can be applied to the selected records and so on. Any number of filters can be applied sequentially. Filters can also be removed using gui_filter_remove button.

gui filter sequential

The sequential filters implementation is based on the ability of DataManager to run sequential queries.

API for Working with Filter Parameters

The Filter interface provides methods for reading and writing of filter parameter values in a screen controller:

  • setParamValue(String paramName, Object value)

  • getParamValue(String paramName)

paramName - filter parameter name. Parameter name is a part of component name that displays a parameter value. The procedure of getting a component name was described above. Parameter name is placed after the last dot in a component name. For example, if the component name is component$filter.model_manufacturer90062, then the parameter name is model_manufacturer90062.

Note that you cannot use these methods in the InitEvent handler of the screen controller, because the filter is not initialized at that moment. A good place to work with filter parameters is the BeforeShowEvent handler.

Full-Text Search Mode in Filter

If a filter data container contains entities that are indexed by the full-text search subsystem (see CUBA Platform. Full Text Search), then a full-text search mode is available in the filter. Use the Full-Text Search checkbox to switch to this mode.

gui filter fts

In the full-text search mode, the filter contains text fields for search criteria, and the search is performed in entity fields indexed by the FTS subsystem.

If a table is defined in the applyTo attribute, then placing the mouse cursor on the table row will display a tooltip with the information what entity attributes satisfy the search criteria.

For hiding the filter mode checkbox, set false value to the modeSwitchVisible filter attribute.

If you want the filter to be opened in the full-text search mode by default, set fts value to the defaultMode filter attribute.

Full-text search can be used combined with any number of filter conditions:

book publication fts filter

The FTS condition can be selected in the conditions selection window.

3.5.2.1.19. Form

The Form component is designed for the joint display and editing of multiple entity attributes. It is a simple container similar to GridLayout, it can have any number of nested columns, the type of nested fields is defined declaratively in XML, the fields' captions are located to the left of the fields. The main difference from GridLayout is that Form enables binding all nested fields to one data container.

Form is used instead of FieldGroup by default in generated editor screens since the framework version 7.0.

gui Form 1

XML-name of the component: form

Below is an example of defining a group of fields in an XML screen descriptor:

<data>
    <instance id="orderDc" class="com.company.sales.entity.Order" view="order-edit">
        <loader/>
    </instance>
</data>
<layout>
    <form id="form" dataContainer="orderDc">
        <dateField property="date"/>
        <textField property="amount" description="Total amount"/>
        <pickerField property="customer"/>

        <field id="statusField" property="status"/>
    </form>
</layout>

In the example above, the form component shows attributes of the entity loaded into the orderDc data container. Nested form elements define visual components bound to entity attributes using the property XML attribute. Captions will be created automatically based on the localized names of entity attributes. The nested components can have any common or specific attributes like description shown in the example.

Apart from concrete visual components, the form can also contain generic fields defined with the nested field element. The framework will choose an appropriate visual component on the basis of the corresponding entity attribute and existing component generation strategies. The field element can have a number of common attributes like description, contextHelpText, etc.

In order to inject a nested component to the screen controller, give it id attribute in XML. The component will be injected with its concrete type, e.g. TextField. If a generic field is injected into the screen controller, it has the Field type which is a superclass of all visual components that can be displayed in the form.

Attributes of form:

  • childrenCaptionWidth – specifies fixed captions width for all nested columns and their child elements. Set -1 to use auto size.

  • captionPosition - defines the fields' caption position: TOP or LEFT.

Elements of form:

  • column – optional element that allows you to position fields in multiple columns. For this purpose, nested fields should be placed not immediately within the form, but within a column. For example:

    <form id="form" dataContainer="orderDc">
        <column width="250px">
            <dateField property="date"/>
            <textField property="amount"/>
        </column>
        <column width="400px">
            <pickerField property="customer"/>
            <textArea property="info"/>
        </column>
    </form>

    In this case, fields will be arranged in two columns; the first column will contain all fields with the width of 250px, the second one with the width of 400px.

    Attributes of column:

    • id – an optional column identifier, which allows you to refer to it in case of screen extension.

    • width – specifies the field width of a column. By default, fields have the width of 200px. In this attribute, the width can be specified both in pixels and in percentage of the total horizontal width of the column.

    • childrenCaptionWidth – specifies fixed captions width for nested fields. Set -1 to use auto size.

Methods of the Form interface:

  • add() - enables adding fields to the Form programmatically. It takes a Component instance as a parameter, and you can also define the position of the new field by adding column and row indexes.

    Data container is not assigned to the components added programmatically, so you have to use the component’s setValueSource() method for data binding.

    For example, if you have declared a form with the name field:

    <data>
        <instance id="customerDc" class="com.company.demo.entity.Customer">
            <loader/>
        </instance>
    </data>
    <layout>
        <form id="form" dataContainer="customerDc">
            <column>
                <textField id="nameField" property="name"/>
            </column>
        </form>
    </layout>

    You can add an email field to the form programmatically in the screen controller as follows:

    @Inject
    private UiComponents uiComponents;
    @Inject
    private InstanceContainer<Customer> customerDc;
    @Inject
    private Form form;
    
    @Subscribe
    private void onInit(InitEvent event) {
        TextField<String> emailField = uiComponents.create(TextField.TYPE_STRING);
        emailField.setCaption("Email");
        emailField.setWidthFull();
        emailField.setValueSource(new ContainerValueSource<>(customerDc, "email"));
        form.add(emailField);
    }


3.5.2.1.20. GroupTable

GroupTable component is a table with an ability to group information dynamically by any field. In order to group a table by a column the required column should be dragged to the left and dropped on the gui_groupTableIcon element of the table header. Grouped values can be expanded and collapsed using gui_groupBox_plus/gui_groupBox_minus buttons.

gui groupTableDragColumn

XML name of the component: groupTable.

A data container of CollectionContainer type or a groupDatasource must be specified for GroupTable, otherwise, grouping will not work. Example:

<data>
    <collection id="ordersDc"
                class="com.company.sales.entity.Order"
                view="order-with-customer">
        <loader id="ordersDl">
            <query>
                <![CDATA[select e from sales_Order e]]>
            </query>
        </loader>
    </collection>
</data>
<layout>
    <groupTable id="ordersTable"
                width="100%"
                dataContainer="ordersDc">
        <columns>
            <group>
                <column id="date"/>
            </group>
            <column id="customer"/>
            <column id="amount"/>
        </columns>
        <rowsCount/>
    </groupTable>
</layout>

group is an optional element that can be present in a single instance inside columns. It contains a set of column elements, by which grouping will be performed initially when opening a screen.

In the example below, we will use the includeAll attribute of the columns element along with the group element.

<groupTable id="groupTable"
            width="100%"
            height="100%"
            dataContainer="customersDc">
    <columns includeAll="true">
        <group>
            <column id="address"/>
        </group>
        <column id="name"
                sortable="false"/>
    </columns>
</groupTable>

So a specific attribute is added to the name column, and a GroupTable is grouped by the address column.

Each column element can contain the groupAllowed attribute with boolean value. This attribute controls whether a user can group by this column.

If aggregatable attribute is true, the table shows aggregation results for each group and results for all rows in an additional row on the top. If showTotalAggregation attribute is false, results for all rows are not shown.

If multiselect attribute is true, the click to the group row holding down the Ctrl key will expand the group (if collapsed) and set the selection to all rows of this group. The converse is not true: if the whole group is selected, Ctrl+click will not deselect all the group. You still can deselect certain rows using the common Ctrl key behaviour.

Methods of the GroupTable interface:
  • groupByColumns() - performs grouping by the given table columns.

    The example below will group the table first by the department name, and then by city:

    groupTable.groupByColumns("department", "city");
  • ungroupByColumns() - resets grouping by the given columns.

    The following example will ungroup the table by department, while grouping by city from the previous snippet will be kept.

    groupTable.ungroupByColumns("department");
  • ungroup() - resets grouping at all.

  • The setAggregationDistributionProvider() method is similar to the same method for the Table component with the only difference that when creating a provider, the GroupAggregationDistributionContext<V> object is used, which contains additional:

    • GroupInfo groupInfo – an object with information about the grouping row: properties of the grouped columns and their values.

  • The getAggregationResults() method returns a map with aggregation results for the specified GroupInfo object, where map keys are table column identifiers, and values are aggregation values.

The rest of the GroupTable functionality is similar to a simple Table.



3.5.2.1.21. Image

Image component is designed for displaying images from different sources. The component can be bound to a data container or configured programmatically.

XML name of the component: image.

The Image component can display the value of an entity attribute of FileDescriptor or byte[] type. In the simplest case image can be declaratively associated with data using the dataContainer and property attributes:

<image id="image" dataContainer="employeeDс" property="avatar"/>

In the example above, component displays the avatar attribute of the Employee entity located in the employeeDс data container.

Alternatively, the Image component can display images from different resources. You can set the resource type declaratively using the image elements listed below:

  • classpath - a resource in the classpath.

    <image>
        <classpath path="com/company/sample/web/screens/myPic.jpg"/>
    </image>
  • file - a resource in the file system.

    <image>
        <file path="D:\sample\modules\web\web\VAADIN\images\myImage.jpg"/>
    </image>
  • relativePath - resource in the application directory.

    <image>
        <relativePath path="VAADIN/images/myImage.jpg"/>
    </image>
  • theme - a theme resource, e.g.,VAADIN/themes/customTheme/some/path/image.png.

    <image>
        <theme path="com.company.sample/myPic.jpg"/>
    </image>
  • url - a resource which can be loaded from the given URL.

    <image>
        <url url="https://www.cuba-platform.com/sites/all/themes/cuba_adaptive/img/lori.png"/>
    </image>

image attributes:

  • scaleMode - applies the scale mode to the image. The following scale modes are available:

    • FILL - the image will be stretched according to the size of the component.

    • CONTAIN - the image will be compressed or stretched to the minimum dimension of the component while preserving the proportions.

    • SCALE_DOWN - the content changes size by comparing the difference between NONE and CONTAIN in order to find the smallest concrete size of the object.

    • NONE - the image will retain its real size.

  • alternateText - sets an alternate text for an image in case the resource is not set or unavailable.

    <image id="image" alternateText="logo"/>

image resources settings:

  • bufferSize - the size of the download buffer in bytes used for this resource.

    <image>
        <file bufferSize="1024" path="C:/img.png"/>
    </image>
  • cacheTime - the length of cache expiration time in milliseconds.

    <image>
        <file cacheTime="2400" path="C:/img.png"/>
    </image>
  • mimeType - the MIME type of the resource.

    <image>
        <url url="https://avatars3.githubusercontent.com/u/17548514?v=4&#38;s=200"
             mimeType="image/png"/>
    </image>

To programmatically manage the Image component, use the following methods:

  • setValueSource() - sets the data container and the entity attribute name. Only FileDescriptor and byte[] attributes are supported.

    The data container can be set programmatically, for example, to display images in table cells:

    frameworksTable.addGeneratedColumn("image", entity -> {
        Image image = uiComponents.create(Image.NAME);
        image.setValueSource(new ContainerValueSource<>(frameworksTable.getInstanceContainer(entity), "image"));
        image.setHeight("100px");
        return image;
    });
    gui Image 1
  • setSource() - sets the content source for the component. The method accepts the resource type and return the resource object that can be configured using the fluent interface. Each resource type has its own methods, for example, setPath() for ThemeResource type or setStreamSupplier() for StreamResource type:

    Image image = uiComponents.create(Image.NAME);
    
    image.setSource(ThemeResource.class)
            .setPath("images/image.png");

    or

    image.setSource(StreamResource.class)
            .setStreamSupplier(() -> new FileDataProvider(fileDescriptor).provide())
            .setBufferSize(1024);

    You can use one of the following resource types implementing the Resource interface or extend it to create a custom resource:

    • ClasspathResource - an image located in classpath. This resource can be also set declaratively using the classpath element of the image component.

    • FileDescriptorResource - an image which can be obtained from the FileStorage using the given FileDescriptor.

    • FileResource - an image stored in the file system. This resource can be also set declaratively using the file element of the image component.

    • RelativePathResource - an image stored in a directory of the application. This resource can be also set declaratively using the relativePath element of the image component.

    • StreamResource - an image from a stream.

    • ThemeResource - a theme image, for example, VAADIN/themes/yourtheme/some/path/image.png. This resource can be also set declaratively using the theme element of the image component.

    • UrlResource - an image which can be loaded from the given URL. This resource can be also set declaratively using the url element of the image component.

  • createResource() - creates the image resource implementation by its type. The created object can be later passed to the setSource() method.

    FileDescriptorResource resource = image.createResource(FileDescriptorResource.class)
            .setFileDescriptor(avatar);
    image.setSource(resource);
  • addClickListener() - adds a listener that will be notified when a user clicks on an image area.

    image.addClickListener(clickEvent -> {
        if (clickEvent.isDoubleClick())
            notifications.create()
                    .withCaption("Double clicked")
                    .show();
    });
  • addSourceChangeListener() - adds a listener that will be notified when a source of an image is changed.



3.5.2.1.22. Label

Label is a text component that displays static text or value of an entity attribute.

XML name of the component: label

Below is an example of setting a label with text taken from the localized message pack:

<label value="msg://orders"/>

The value attribute sets text for a label.

In a web client, the text contained in value will be split into multiple lines if its length exceeds the width value. Therefore, to display a multiline label, it is sufficient to specify an absolute value of width. If the label text is too long and the value of width is not specified, the text will be truncated.

<label value="Label, which should be split into multiple lines"
       width="200px"/>

You can set label parameters in the screen controller. To do this, you should specify a component identifier and get a reference to the component in the controller:

<label id="dynamicLabel"/>
@Inject
private Label dynamicLabel;

@Subscribe
protected void onInit(InitEvent event) {
    dynamicLabel.setValue("Some value");
}

The Label component can display value of an entity attribute. For this purpose, dataContainer and property attributes are used. For example:

<data>
    <instance id="customerDc" class="com.company.sales.entity.Customer" view="_local">
        <loader/>
    </instance>
</data>
<layout>
    <label dataContainer="customerDc" property="name"/>
</layout>

In the example above, component displays the name attribute of the Customer entity located in the customerDc data container.

htmlEnabled attribute indicates the way the value attribute will be interpreted: if htmlEnabled="true", the attribute will be treated as HTML code, otherwise as a plain string.

Label styles

In Web Client with a Halo-based theme, you can set predefined styles to the Label component using the stylename attribute either in the XML descriptor or in the screen controller:

<label value="Label to be styled"
       stylename="colored"/>

When setting a style programmatically, select one of the HaloTheme class constants with the LABEL_ prefix:

label.setStyleName(HaloTheme.LABEL_COLORED);
  • bold - bolder font weight. Suitable for important/prominent UI text.

  • colored - colored text.

  • failure - failure badge style. Adds a border around the label and an icon next to the text. Suitable for UI notifications that need to be used in the direct context of some component.

  • h1 - header style for main application headings.

  • h2 - header style for different sections in the application.

  • h3 - header style for different sub-sections in the application.

  • h4 - header style for different sub-sections in the application.

  • light - lighter font weight. Suitable for additional/supplementary UI text.

  • no-margin - removes default margins from the header.

  • spinner - spinner style. Add this style name to an empty Label to create a spinner.

  • success - success badge style. Adds a border around the label and an icon next to the text. Suitable for UI notifications that need to be used in the direct context of some component.


Attributes of label

align - css - dataContainer - datasource - description - descriptionAsHtml - enable - box.expandRatio - height - htmlEnabled - icon - id - property - stylename - value - visible - width

Elements of label

formatter

Predefined styles of label

bold - colored - failure - h1 - h2 - h3 - h4 - huge - large - light - no-margin - small - spinner - success - tiny

API

addValueChangeListener


Link is a hyperlink, which enables uniform opening of external web resources.

XML-name of the component: link

An example of XML-description for link:

<link caption="Link" url="https://www.cuba-platform.com" target="_blank" rel="noopener"/>

link attributes:



3.5.2.1.24. LinkButton

LinkButton is a button that looks like a hyperlink.

XML name of the component: linkButton

The link button can contain text or icon (or both). The figure below shows different types of buttons.

gui linkButtonTypes

The link button differs from regular Button only in its appearance. All properties and behavior are identical to those described for Button.

Below is an example of XML description of a link button that invokes the someMethod() method of a controller. The link button has a caption (the caption attribute), a tooltip (the description attribute) and an icon (the icon attribute):

<linkButton id="linkButton"
            caption="msg://linkButton"
            description="Press me"
            icon="SAVE"
            invoke="someMethod"/>


3.5.2.1.25. LookupField

This is a component to select a value from drop-down list. Drop-down list provides the filtering of values as the user inputs some text, and the pagination of available values.

gui lookupField

XML name of the component: lookupField.

  • The simplest case of using LookupField is to select an enumeration value for an entity attribute. For example, a Role entity has a type attribute of the RoleType type, which is an enumeration. Then you can use LookupField to edit this attribute as follows:

    <data>
        <instance id="roleDc"
                  class="com.haulmont.cuba.security.entity.Role"
                  view="_local">
            <loader/>
        </instance>
    </data>
    <layout expand="editActions" spacing="true">
        <lookupField dataContainer="roleDc" property="type"/>
    </layout>

    In the example above, the screen defines roleDc data container for the Role entity. In the lookupField component, a link to a data container is specified in the dataContainer attribute, and a name of an entity attribute is specified in the property attribute. In this case, the entity attribute is an enumeration, and the drop-down list will display localized names of all enumeration values.

  • Similarly, LookupField can be used to select an instance of a related entity. optionsContainer attribute is used to create a list of options:

    <data>
        <instance id="carDc" class="com.haulmont.sample.core.entity.Car" view="carEdit">
            <loader/>
        </instance>
        <collection id="colorsDc" class="com.haulmont.sample.core.entity.Color" view="_minimal">
            <loader id="colorsDl">
                <query>
                    <![CDATA[select e from sample_Color e]]>
                </query>
            </loader>
        </collection>
    </data>
    <layout>
        <lookupField dataContainer="carDc" property="color" optionsContainer="colorsDc"/>
    </layout>

    In this case, the component will display instance names of the Color entity located in the colorsDc data container, and the selected value will be set into the color attribute of the Car entity, which is located in the carDc data container.

    captionProperty attribute defines which entity attribute can be used instead of an instance name for string option names.

  • The setOptionCaptionProvider() method allows you to define captions for string option names displayed by LookupField component:

    lookupField.setOptionCaptionProvider((item) -> item.getLocalizedName());
  • The list of component options can be specified arbitrarily using the setOptionsList(), setOptionsMap() and setOptionsEnum() methods, or using the XML optionsContainer or optionsDatasource attribute.

    • setOptionsList() allows you to programmatically specify a list of component options. To do this, declare a component in the XML descriptor:

      <lookupField id="numberOfSeatsField" dataContainer="modelDc" property="numberOfSeats"/>

      Then inject the component into the controller and specify a list of options in the onInit() method:

      @Inject
      protected LookupField<Integer> numberOfSeatsField;
      
      @Subscribe
      public void onInit(InitEvent event) {
          List<Integer> list = new ArrayList<>();
          list.add(2);
          list.add(4);
          list.add(5);
          list.add(7);
          numberOfSeatsField.setOptionsList(list);
      }

      In the component’s drop-down list the values 2, 4, 5 and 7 will be displayed. Selected number will be put into the numberOfSeats attribute of an entity located in the modelDc data container.

    • setOptionsMap() allows you to specify string names and option values separately. For example, in the numberOfSeatsField component in the XML descriptor, specify an option map in onInit():

      @Inject
      protected LookupField<Integer> numberOfSeatsField;
      
      @Subscribe
      public void onInit(InitEvent event) {
          Map<String, Integer> map = new LinkedHashMap<>();
          map.put("two", 2);
          map.put("four", 4);
          map.put("five", 5);
          map.put("seven", 7);
          numberOfSeatsField.setOptionsMap(map);
      }

      In the component’s drop-down list, two, four, five, seven strings will be displayed. However, the value of the component will be a number that corresponds to the selected option. It will be put into the numberOfSeats attribute of an entity located in the modelDc data container.

    • setOptionsEnum() takes the class of an enumeration as a parameter. The drop-down list will show localized names of enum values, the value of the component will be an enum value.

  • setPopupWidth() allows you to set the drop-down list’s width, which is passed to the method as a string. By using relative units (e.g., "50%") it’s possible to set the drop-down list’s width relative to the LookupField itself. By default, this width is set to null so that the drop-down list’s width can be greater than a component width to fit the content of all displayed items. By setting the value to "100%" the drop-down list’s width will be equal to the width of the LookupField.

  • setOptionStyleProvider() enables you to use separate style names for the the options displayed by the component:

    lookupField.setOptionStyleProvider(entity -> {
        User user = (User) entity;
        switch (user.getGroup().getName()) {
            case "Company":
                return "company";
            case "Premium":
                return "premium";
            default:
                return "company";
        }
    });
  • Each drop-down list component can have an icon on the left. Use the setOptionIconProvider() method in the screen controller:

    lookupField.setOptionIconProvider(entity -> {
        if (entity.getType() == LegalStatus.LEGAL)
            return "icons/icon-office.png";
        return "icons/icon-user.png";
    });
    gui lookupField 2

    If you use SVG icons, set the icon size explicitly to avoid icons overlay.

    <svg version="1.1"
         id="Capa_1"
         xmlns="http://www.w3.org/2000/svg"
         xmlns:xlink="http://www.w3.org/1999/xlink"
         xml:space="preserve"
    
         style="enable-background:new 0 0 55 55;"
         viewBox="0 0 55 55"
    
         height="25px"
         width="25px">
  • If the LookupField component is not required and if the related entity attribute is not declared as required, the list of component options has an empty row. If this row is selected, the component returns null. The nullName attribute allows you to specify a row to be displayed in this case instead of an empty one. Below is an example:

    <lookupField dataContainer="carDc" property="colour" optionsContainer="colorsDs" nullName="(none)"/>

    In this case, instead of an empty row, (none) will be displayed. If this row is selected, null will be set to a related entity attribute.

    If you specify a list of options programmatically using setOptionsList(), you can pass one of the options into setNullOption() method. Then, if the user selects it, the component value will be null.

    LookupField filters:
    • Using the filterMode attribute, option filtering type can be defined for the user input:

      • NO − no filtering.

      • STARTS_WITH − by the beginning of a phrase.

      • CONTAINS − by any occurrence (is used by default).

    • The setFilterPredicate() method enables to setup how items should be filtered. The predicate tests whether an item with the given caption matches to the given search string. For example:

      BiFunction<String, String, Boolean> predicate = String::contains;
      lookupField.setFilterPredicate((itemCaption, searchString) ->
              predicate.apply(itemCaption.toLowerCase(), searchString));

      The functional interface FilterPredicate has the test method that enables implementing custom filtering logic, for example, to escape accents or special characters:

      lookupField.setFilterPredicate((itemCaption, searchString) ->
              StringUtils.replaceChars(itemCaption, "ÉÈËÏÎ", "EEEII")
                  .toLowerCase()
                  .contains(searchString));
  • The LookupField component is able to handle user input if there is no suitable option in the list. In this case, setNewOptionHandler() is used. For example:

    @Inject
    private Metadata metadata;
    @Inject
    private LookupField<Color> colorField;
    @Inject
    private CollectionContainer<Color> colorsDc;
    
    @Subscribe
    protected void onInit(InitEvent event) {
        colorField.setNewOptionHandler(caption -> {
            Color color = metadata.create(Color.class);
            color.setName(caption);
            colorsDc.getMutableItems()
                    .add(color);
            colorField.setValue(color);
        });
    }

    The new options handler is invoked if the user enters a value that does not coincide with any option and presses Enter. In this case, a new Color entity instance is created in the handler, its name attribute is set to the value entered by the user, this instance is added to the options data container and selected in the component.

    Instead of using setNewOptionHandler() method for processing user input, the controller method name can be specified in the newOptionHandler XML attribute. This method should have two parameters, one of LookupField type, and the other of String type. They will be set to the component instance and the value entered by the user, accordingly. The newOptionAllowed attribute is used to enable adding new options.

  • The nullOptionVisible XML attribute sets visibility for the null option in the drop-down list. It allows you to make LookupField not required but still without the null option.

  • The textInputAllowed XML attribute can be used to disable filtering options from keyboard. It can be convenient for short lists. The default value is true.

  • The pageLength XML attribute allows you to redefine the number of options on one page of the drop-down list, specified in the cuba.gui.lookupFieldPageLength application property.

  • In Web Client with a Halo-based theme, you can set predefined styles to the LookupField component using the stylename attribute either in the XML descriptor or in the screen controller:

    <lookupField id="lookupField"
                 stylename="borderless"/>

    When setting a style programmatically, select one of the HaloTheme class constants with the LOOKUPFIELD_ prefix:

    lookupField.setStyleName(HaloTheme.LOOKUPFIELD_BORDERLESS);

    LookupField styles:

    • align-center - align the text inside the field to center.

    • align-right - align the text inside the field to the right.

    • borderless - removes the border and background from the text field.



3.5.2.1.26. LookupPickerField

The LookupPickerField component enables to display an entity instance in a text field, select an instance in a drop-down list and perform actions by pressing buttons on the right.

gui lookupPickerField

XML name of the component: lookupPickerField.

In fact, LookupPickerField is a hybrid of LookupField and PickerField. Thus it has the same features except the default list of actions added when determining the component in XML: for LookupPickerField these are lookup lookupBtn and open openBtn actions.

Below is an example of using LookupPickerField to select a value of the color reference attribute of the Car entity:

<data>
    <instance id="carDc" class="com.haulmont.sample.core.entity.Car" view="carEdit">
        <loader/>
    </instance>
    <collection id="colorsDc" class="com.haulmont.sample.core.entity.Color" view="_minimal">
        <loader id="colorsDl">
            <query>
                <![CDATA[select e from sample_Color e]]>
            </query>
        </loader>
    </collection>
</data>
<layout>
    <lookupPickerField dataContainer="carDc" property="color" optionsContainer="colorsDc"/>
</layout>


3.5.2.1.27. MaskedField

This is a text field, in which data is entered in a predefined format. For example, it is convenient to use MaskedField to enter telephone numbers.

XML name of the component: maskedField.

The MaskedField component is implemented for Web Client only.

Basically, MaskedField repeats the functionality of TextField, except that you cannot set datatype for it. So, MaskedField is intended for work only with text and entity attributes of type String. MaskedField has the following specific attributes:

  • mask – sets a mask for the field. To set a mask, use the following characters:

    • # – number

    • U – uppercase letter

    • L – lowercase letter

    • ? – letter

    • А – letter or number

    • * – any character

    • H – uppercase hex character

    • h – lowercase hex character

    • ~ – " +" or "-" character

  • valueMode – defines a format of a returned value (with a mask or not) and can take either masked or clear.

Example of a text field with a mask for entering telephone numbers is provided below:

<maskedField id="phoneNumberField" mask="(###)###-##-##" valueMode="masked"/>
<button id="showPhoneNumberBtn" caption="msg://showPhoneNumberBtn"/>
@Inject
private MaskedField phoneNumberField;
@Inject
private Notifications notifications;

@Subscribe("showPhoneNumberBtn")
protected void onShowPhoneNumberBtnClick(Button.ClickEvent event) {
    notifications.create()
            .withCaption((String) phoneNumberField.getValue())
            .withType(Notifications.NotificationType.HUMANIZED)
            .show();
}
gui MaskedField
gui MaskedField maskedValueMode


3.5.2.1.28. OptionsGroup

This is a component that allows a user to choose from a list of options. Radio buttons are used to select a single value; a group of checkboxes is used to select multiple values.

gui optionsGroup

XML name of the component: optionsGroup.

  • The simplest case of using OptionsGroup is to select an enumeration value for an entity attribute. For example, a Customer entity has the grade attribute of the CustomerGrade type, which is an enumeration. Then you can use OptionsGroup to edit this attribute as follows:

    <data>
        <instance id="customerDc"
                  class="com.company.app.entity.Customer"
                  view="_local">
            <loader/>
        </instance>
    </data>
    <layout>
        <optionsGroup id="gradeField" property="grade" dataContainer="customerDc"/>
    </layout>

    In the example above, the customerDc data container is defined for the Customer entity. In the optionsGroup component, the link to the data container is specified in the dataContainer attribute and the name of the entity attribute is set in the property attribute.

    As a result, the component will be as follows:

gui optionsGroup customerGrade
  • The list of component options can be specified arbitrarily using the setOptionsList(), setOptionsMap() and setOptionsEnum() methods, or using the XML optionsDatasource or optionsEnum attributes.

  • setOptionsList() allows you to specify programmatically a list of component options. To do this, declare a component in the XML descriptor:

    <optionsGroup id="optionsGroupWithList"/>

    Then inject the component into the controller and specify a list of options in the onInit() method:

    @Inject
    private OptionsGroup<Integer, Integer> optionsGroupWithList;
    
    @Subscribe
    protected void onInit(InitEvent event) {
        List<Integer> list = new ArrayList<>();
        list.add(2);
        list.add(4);
        list.add(5);
        list.add(7);
        optionsGroupWithList.setOptionsList(list);
    }

    The component will be as follows:

    gui optionsGroup integerList

    Depending on the selected option, the getValue() method of the component will return Integer values: 2, 4, 5, 7.

  • setOptionsMap() allows you to specify string names and option values separately. For example, we can set the following options map for the optionsGroupWithMap component, described the XML descriptor, in the onInit() method of the controller:

    @Inject
    private OptionsGroup<Integer, Integer> optionsGroupWithMap;
    
    @Subscribe
    protected void onInit(InitEvent event) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("two", 2);
        map.put("four", 4);
        map.put("five", 5);
        map.put("seven", 7);
        optionsGroupWithMap.setOptionsMap(map);
    }

    The component will be as follows:

    gui optionsGroup integerMap

    Depending on the selected option, the getValue() method of the component will return Integer values: 2, 4, 5, 7, and not the strings that are displayed on the screen.

  • setOptionsEnum() takes a class of enumeration as a parameter. The options list will consist of localized names of enum values, the value of the component will be an enum value.

  • The component can take a list of options from a data container. For this purpose, the optionsContainer attribute is used. For example:

    <data>
        <collection id="coloursDc"
                    class="com.haulmont.app.entity.Colour"
                    view="_local">
            <loader id="coloursLoader">
                <query>
                    <![CDATA[select c from app_Colour c]]>
                </query>
            </loader>
        </collection>
    </data>
    <layout>
        <optionsGroup id="coloursField" optionsContainer="coloursDc"/>
    </layout>

    In this case, the coloursField component will display instance names of the Colour entity, located in the coloursDc data container, and its getValue() method will return the selected entity instance.

    With the help of the captionProperty attribute entity attribute to be used instead of an instance name for a string option names can be defined.

  • The multiselect attribute is used to switch OptionsGroup to a multiple-choice mode. If multiselect is turned on, the component is displayed as a group of independent checkboxes, and the component value is a list of selected options.

    For example, if we create the component in the XML screen descriptor:

    <optionsGroup id="roleTypesField" multiselect="true"/>

    and set a list of options for it – RoleType enumeration values:

    @Inject
    protected OptionsGroup roleTypesField;
    
    @Subscribe
    protected void onInit(InitEvent event) {
        roleTypesField.setOptionsList(Arrays.asList(RoleType.values()));
    }

    then the component will be as follows:

    gui optionsGroup roleType multi

    In this case, the getValue() method of the component will return a java.util.List, containing RoleType.READONLY and RoleType.DENYING values.

    The example above also illustrates the ability of the OptionsGroup component to display localized values of enumerations included in the data model.

    You can also make some values selected programmatically by passing a java.util.List of values to the setValue() method:

    optionsGroup.setValue(Arrays.asList(RoleType.STANDARD, RoleType.ADMIN));
  • The orientation attribute defines the orientation of group elements. By default, elements are arranged vertically. The horizontal value sets the horizontal orientation.

The appearance of the OptionsGroup component can be customized using SCSS variables with $cuba-optiongroup-* prefix. You can change these variables in the visual editor after creating a theme extension or a custom theme.



3.5.2.1.29. OptionsList

OptionsList is a variation of the OptionsGroup component which represents the list of options as a vertical scrollable list. If multi-select is enabled, multiple items can be selected by holding the Ctrl key while clicking, or as a range by holding the Shift key.

gui optionsList

XML name of the component: optionsList.

The OptionsList component is implemented for Web Client.

By default the OptionsList component displays the first null element in suggestion popup, this can be disabled with the help of nullOptionVisible attribute set to false.

The addDoubleClickListener() method allows you to listen to DoubleClickEvent which is sent when a user double-clicks on the component options.

optionsList.addDoubleClickListener(doubleClickEvent ->
        notifications.create()
        .withCaption("Double clicked")
        .show());

For the same purpose, you can subscribe to the event in the screen controller, for example:

@Subscribe("optionsList")
private void onOptionsListDoubleClick(OptionsList.DoubleClickEvent event) {
    notifications.create()
            .withCaption("Double clicked")
            .show();
}

The only difference in API between OptionsList and OptionsGroup is that OptionsList has no orientation attribute.



3.5.2.1.30. PasswordField

This is a text field that displays echo characters instead of those entered by a user.

XML name of the component: passwordField.

Basically, PasswordField is similar to TextField apart from the ability to set datatype. PasswordField is intended to work with text and entity attributes of type String only.

Example:

<passwordField id="passwordField" caption="msg://name"/>
<button id="showPasswordBtn" caption="msg://buttonsName"/>
@Inject
private PasswordField passwordField;
@Inject
private Notifications notifications;

@Subscribe("showPasswordBtn")
protected void onShowPasswordBtnClick(Button.ClickEvent event) {
    notifications.create()
            .withCaption(passwordField.getValue())
            .show();
}
gui PasswordField

The autocomplete attribute allows you to enable saving passwords in the web browser. It is disabled by default.

The capsLockIndicator attribute allows you to set the id of a CapsLockIndicator component that should indicate Caps Lock state for this passwordField. The Caps Lock state is handled only when the passwordField is focused. When the field loses its focus, the state changes to "Caps Lock off".

Example:

<passwordField id="passwordField"
               capsLockIndicator="capsLockIndicator"/>
<capsLockIndicator id="capsLockIndicator"
                   align="MIDDLE_CENTER"
                   capsLockOffMessage="Caps Lock is OFF"
                   capsLockOnMessage="Caps Lock is ON"/>


3.5.2.1.31. PickerField

PickerField displays an entity instance in a text field and performs actions when a user clicks buttons on the right.

PickerField

XML name of the component: pickerField.

  • As a rule, PickerField is used for working with reference entity attributes. It is sufficient to specify dataContainer and property attributes for the component:

    <data>
        <instance id="carDc" class="com.haulmont.sample.core.entity.Car" view="carEdit">
            <loader/>
        </instance>
    </data>
    <layout>
        <pickerField dataContainer="carDc" property="color"/>
    </layout>

    In the example above, the screen defines carDc data container for a Car entity having the color attribute. In the pickerField element, a link to a data container is specified in the dataContainer attribute, and a name of an entity attribute is set in the property attribute. The entity attribute should refer to another entity, in the example above it is Color.

  • For PickerField, you can define an arbitrary number of actions, displayed as buttons on the right.

    It can be done either in the XML descriptor using the actions nested element, or programmatically in the controller using addAction().

    • The framework provides a set of standard PickerField actions: picker_lookup, picker_clear, picker_open. They perform the selection of a related entity, clearing the field and opening the edit screen for a selected related entity, respectively. When declaring standard actions in XML, you should define the action identifier and its type using the type attribute.

      If no actions are defined in the actions element when declaring the component, the XML loader will define lookup and clear actions for it. To add a default action, for example, open, you need to define the actions element as follows:

      <pickerField dataContainer="carDc" property="color">
          <actions>
              <action id="lookup" type="picker_lookup"/>
              <action id="open" type="picker_open"/>
              <action id="clear" type="picker_clear"/>
          </actions>
      </pickerField>

      The action element does not extend but overrides a set of standard actions. Identifiers of all required actions have to be defined explicitly. The component looks as follows:

      gui pickerFieldActionsSt

      Use addAction() to set standard actions programmatically. If the component is defined in the XML descriptor without actions nested element, it is sufficient to add missing actions:

      @Inject
      protected PickerField<Color> colorField;
      
      @Subscribe
      protected void onInit(InitEvent event) {
          colorField.addAction(actions.create(OpenAction.class));
      }

      If the component is created in the controller, it will get no default actions and you need to explicitly add all necessary actions:

      @Inject
      private InstanceContainer<Car> carDc;
      @Inject
      private UiComponents uiComponents;
      @Inject
      private Actions actions;
      
      @Subscribe
      protected void onInit(InitEvent event) {
          PickerField<Color> colorField = uiComponents.create(PickerField.NAME);
          colorField.setValueSource(new ContainerValueSource<>(carDc, "color"));
          colorField.addAction(actions.create(LookupAction.class));
          colorField.addAction(actions.create(OpenAction.class));
          colorField.addAction(actions.create(ClearAction.class));
          getWindow().add(colorField);
      }

      You can customize the standard actions behavior by subscribing to the ActionPerformedEvent and providing your own implementation. For example, you can use a specific lookup screen as follows:

      @Inject
      private ScreenBuilders screenBuilders;
      @Inject
      private PickerField<Color> pickerField;
      
      @Subscribe("pickerField.lookup")
      protected void onPickerFieldLookupActionPerformed(Action.ActionPerformedEvent event) {
              screenBuilders.lookup(pickerField)
                       .withScreenClass(CustomColorBrowser.class)
                       .build()
                       .show();
      }

      For more information, see Opening Screens section.

    • Arbitrary actions in the XML descriptor can also be defined in the actions nested element, and the action logic should be implemented in this action’s event, for example:

      <pickerField dataContainer="orderDc" property="customer">
          <actions>
              <action id="lookup"/>
              <action id="show" icon="PICKERFIELD_OPEN" caption="Show"/>
          </actions>
      </pickerField>
      @Inject
      private PickerField<Customer> pickerField;
      
      @Subscribe("pickerField.show")
      protected void onPickerFieldShowActionPerformed(Action.ActionPerformedEvent event) {
          CustomerEdit customerEdit = screenBuilders.editor(pickerField)
                  .withScreenClass(CustomerEdit.class)
                  .build();
          customerEdit.setDiscount(true);
          customerEdit.show();
      }

      The declarative and programmatic creation of actions is described in Actions. The Action Interface section.

  • PickerField can be used without binding to entities, i.e., without setting dataContainer/datasource and property. In this case, metaClass attribute should be used to specify an entity type for PickerField. For example:

    <pickerField id="colorField" metaClass="sample_Color"/>

    You can get an instance of a selected entity by injecting the component into a controller and invoking its getValue() method.

    For proper operation of the PickerField component you need either set a metaClass attribute, or simultaneously set dataContainer/datasource and property attributes.

  • You can use keyboard shortcuts in PickerField, see Keyboard Shortcuts for details.

  • PickerField component can have an icon on the left. Below is an example of using a function in the setOptionIconProvider() method in the screen controller. The "cancel" icon should be installed when a field value equals null; else the "chain" icon should be installed.

    @Inject
    private PickerField<Customer> pickerField;
    
    protected String generateIcon(Customer customer) {
        return (customer!= null) ? "icons/chain.png" : "icons/cancel.png";
    }
    
    @Subscribe
    private void onInit(InitEvent event) {
        pickerField.setOptionIconProvider(this::generateIcon);
    }
    gui pickerField icons


3.5.2.1.32. PopupButton

This is a button with a popup. Popup may contain a drop-down list of actions or a custom content.

PopupButton

XML name of the component: popupButton.

PopupButton can contain text, which is specified using the caption attribute, or icon (or both). A tooltip can be defined in the description attribute. The figure below shows different types of buttons:

gui popupButtonTypes

popupButton elements:

  • actions - specifies the drop-down actions list.

    Only the following action properties are displayed: caption, enable, visible. The description and shortcut properties are ignored. Handling of the icon property depends on the cuba.gui.showIconsForPopupMenuActions application property and the showActionIcons attribute of the component. The latter has priority.

    Below is an example of a button with a drop-down list containing two actions:

    <popupButton id="popupButton" caption="msg://popupButton" description="Press me">
        <actions>
            <action id="popupAction1" caption="msg://action1"/>
            <action id="popupAction2" caption="msg://action2"/>
        </actions>
    </popupButton>

    You can create the actions from scratch or use the actions already defined for any element in the current screen, for example:

    <popupButton id="popupButton">
        <actions>
            <action id="ordersTable.create"/>
            <action id="ordersTable.edit"/>
            <action id="ordersTable.remove"/>
        </actions>
    </popupButton>
  • popup - sets custom inner content for the popup. Actions are ignored if a custom popup content is set.

    Below is an example of a custom popup layout:

    <popupButton id="popupButton"
                 caption="Settings"
                 align="MIDDLE_CENTER"
                 icon="font-icon:GEARS"
                 closePopupOnOutsideClick="true"
                 popupOpenDirection="BOTTOM_CENTER">
        <popup>
            <vbox width="250px"
                  height="AUTO"
                  spacing="true"
                  margin="true">
                <label value="Settings"
                       align="MIDDLE_CENTER"
                       stylename="h2"/>
                <progressBar caption="Progress"
                             width="100%"/>
                <textField caption="New title"
                           width="100%"/>
                <lookupField caption="Status"
                             optionsEnum="com.haulmont.cuba.core.global.SendingStatus"
                             width="100%"/>
                <hbox spacing="true">
                    <button caption="Save" icon="SAVE"/>
                    <button caption="Reset" icon="REMOVE"/>
                </hbox>
            </vbox>
        </popup>
    </popupButton>
    gui popupButton custom

popupButton attributes:

  • autoClose - defines if the popup should be closed automatically after the action triggering.

  • closePopupOnOutsideClick - if set to true, clicking on the outside the popup closes it. This does not affect clicking on the button itself.

  • menuWidth - sets the popup menu width.

  • popupOpenDirection - sets the opening direction for the popup. Possible values:

    • BOTTOM_LEFT,

    • BOTTOM_RIGHT,

    • BOTTOM_CENTER.

  • showActionIcons - enables displaying icons for action buttons.

  • togglePopupVisibilityOnClick - defines whether sequential click on the popup should toggle popup visibility.

Methods of the PopupButton interface:

  • addPopupVisibilityListener() - adds a listener to intercept the events of the component’s visibility changes.

    popupButton.addPopupVisibilityListener(popupVisibilityEvent ->
            notifications.create()
                    .withCaption("Popup visibility changed")
                    .show());

    You can also track PopupButton visibility changes by subscribing to the corresponding event:

    @Subscribe("popupButton")
    protected void onPopupButtonPopupVisibility(PopupButton.PopupVisibilityEvent event) {
        notifications.create()
                .withCaption("Popup visibility changed")
                .show();
    }

The appearance of the PopupButton component can be customized using SCSS variables with $cuba-popupbutton-* prefix. You can change these variables in the visual editor after creating a theme extension or a custom theme.



3.5.2.1.33. PopupView

PopupView is a component that allows you to open a popup with a container. The popup can be opened by clicking on minimized value or programmatically. It can be closed by mouse out or by clicking on outside area.

A typical PopupView with hidden and visible popup is shown below:

Popup hidden
Figure 18. Popup hidden
Popup visible
Figure 19. Popup visible

An example of a PopupView with minimized value retrieved from a localized message pack:

<popupView id="popupView"
           minimizedValue="msg://minimizedValue"
           caption="PopupView caption">
    <vbox width="60px" height="40px">
        <label value="Content" align="MIDDLE_CENTER"/>
    </vbox>
</popupView>

The inner content of the PopupView should be a container, for example BoxLayout.

PopupView methods:

  • setPopupVisible() allows you to open popup window programmatically.

    @Inject
    private PopupView popupView;
    
    @Subscribe
    protected void onInit(InitEvent event) {
        popupView.setMinimizedValue("Hello world!");
    }
  • setMinimizedValue() allows you to set a minimized value programmatically.

    @Inject
    private PopupView popupView;
    
    @Override
    public void init(Map<String, Object> params) {
        popupView.setMinimizedValue("Hello world!");
    }
  • addPopupVisibilityListener(PopupVisibilityListener listener) allows you to listen to the popup window visibility changes.

    @Inject
    private PopupView popupView;
    @Inject
    private Notifications notifications;
    
    @Subscribe
    protected void onInit(InitEvent event) {
        popupView.addPopupVisibilityListener(popupVisibilityEvent ->
                notifications.create()
                        .withCaption(popupVisibilityEvent.isPopupVisible() ? "The popup is visible" : "The popup is hidden")
                        .withType(Notifications.NotificationType.HUMANIZED)
                        .show()
        );
    }

PopupView attributes:

  • minimizedValue attribute defines the text of popup button. This text may contain HTML tags.

  • If the hideOnMouseOut attribute is set to false, popup container will close after click on outside area.



3.5.2.1.34. ProgressBar

The ProgressBar component is designed to display the progress of a long process.

gui progressBar

XML name of the component: progressBar

Below is an example of the component usage together with the background tasks mechanism:

<progressBar id="progressBar" width="100%"/>
@Inject
private ProgressBar progressBar;
@Inject
private BackgroundWorker backgroundWorker;

private static final int ITERATIONS = 5;

@Subscribe
protected void onInit(InitEvent event){
    BackgroundTask<Integer, Void> task = new BackgroundTask<Integer, Void>(300, getWindow()) {
        @Override
        public Void run(TaskLifeCycle<Integer> taskLifeCycle) throws Exception{
            for(int i = 1; i <= ITERATIONS; i++) {
                TimeUnit.SECONDS.sleep(2); (1)
                taskLifeCycle.publish(i);
            }
            return null;
        }

        @Override
        public void progress(List<Integer> changes){
            double lastValue = changes.get(changes.size() - 1);
            progressBar.setValue((lastValue / ITERATIONS));
        }
    };

    BackgroundTaskHandler taskHandler = backgroundWorker.handle(task);
    taskHandler.execute();
}
1 some time consuming task

Here in the BackgroundTask.progress() method, which is executed in UI thread, the ProgressBar component is set to the current value. The component value should be a double number from 0.0 to 1.0.

The changes of the ProgressBar value can be tracked using the ValueChangeListener. The origin of the ValueChangeEvent can be tracked using isUserOriginated() method.

If a running process is unable to send information about the progress, an indeterminate state of the indicator can be displayed. Set the indeterminate to true to show an indeterminate state. Default is false. For example:

<progressBar id="progressBar" width="100%" indeterminate="true"/>

By default indeterminate progress bar is displayed as horizontal bar. To make it a spinning wheel instead, set the attribute stylename="indeterminate-circle".

To make the progress bar indicator appear as a dot which progresses over the progress bar track (instead of a growing bar), use the point predefined style:

progressBar.setStyleName(HaloTheme.PROGRESSBAR_POINT);


3.5.2.1.35. RadioButtonGroup

This is a component that allows a user to select a single value from a list of options using radio buttons.

gui RadioButtonGroup

XML name of the component: radioButtonGroup.

The RadioButtonGroup component is implemented for Web Client.

The list of component options can be specified using the setOptions(), setOptionsList(), setOptionsMap() and setOptionsEnum() methods, or using an optionsDatasource or optionsContainer attribute.

  • The simplest case of using RadioButtonGroup is to select an enumeration value for an entity attribute. For example, a Role entity has type attribute of the RoleType type, which is an enumeration. Then you can use RadioButtonGroup to display this attribute as follows, using the optionsEnum attribute:

    <radioButtonGroup optionsEnum="com.haulmont.cuba.security.entity.RoleType"
                      property="type"/>

    The setOptionsEnum() takes a class of enumeration as a parameter. The options list will consist of localized names of enum values, the value of the component will be an enum value.

    radioButtonGroup.setOptionsEnum(RoleType.class);

    The same result will be achieved using the setOptions() method which enables working with all types of options:

    radioButtonGroup.setOptions(new EnumOptions<>(RoleType.class));
  • setOptionsList() enables specifying programmatically a list of component options. To do this, declare a component in the XML descriptor:

    <radioButtonGroup id="radioButtonGroup"/>

    Then inject the component into the controller and specify a list of options for it:

    @Inject
    private RadioButtonGroup<Integer> radioButtonGroup;
    
    @Subscribe
    protected void onInit(InitEvent event) {
        List<Integer> list = new ArrayList<>();
        list.add(2);
        list.add(4);
        list.add(5);
        list.add(7);
        radioButtonGroup.setOptionsList(list);
    }

    The component will be as follows:

    gui RadioButtonGroup 2

    Depending on the selected option, the getValue() method of the component will return Integer values: 2, 4, 5, 7.

  • setOptionsMap() enables specifying string names and option values separately. For example, we can set the following options map for the radioButtonGroup component injected in the controller:

    @Inject
    private RadioButtonGroup<Integer> radioButtonGroup;
    
    @Subscribe
    protected void onInit(InitEvent event) {
        Map<String, Integer> map = new LinkedHashMap<>();
        map.put("two", 2);
        map.put("four", 4);
        map.put("five", 5);
        map.put("seven", 7);
        radioButtonGroup.setOptionsMap(map);
    }

    The component will be as follows:

    gui RadioButtonGroup 3

    Depending on the selected option, the getValue() method of the component will return Integer values: 2, 4, 5, 7, and not the strings that are displayed on the screen.

  • The component can take a list of options from a data container. For this purpose, the optionsContainer attribute is used. For example:

    <data>
        <collection id="employeesCt" class="com.company.demo.entity.Employee" view="_minimal">
            <loader>
                <query><![CDATA[select e from demo_Employee e]]></query>
            </loader>
        </collection>
    </data>
    <layout>
        <radioButtonGroup optionsContainer="employeesCt"/>
    </layout>

    In this case, the radioButtonGroup component will display instance names of the Employee entity, located in the employeesCt data container, and its getValue() method will return the selected entity instance.

    gui RadioButtonGroup 4

    With the help of captionProperty attribute entity attribute to be used instead of an instance name for string option names can be defined.

    Programmatically, you can define the options container using the setOptions() method of RadioButtonGroup interface:

    @Inject
    private RadioButtonGroup<Employee> radioButtonGroup;
    @Inject
    private CollectionContainer<Employee> employeesCt;
    
    @Subscribe
    protected void onInit(InitEvent event) {
        radioButtonGroup.setOptions(new ContainerOptions<>(employeesCt));
    }

The orientation attribute defines the orientation of group elements. By default, elements are arranged vertically. The horizontal value sets the horizontal orientation.



3.5.2.1.36. RelatedEntities

RelatedEntities component is a popup button with a drop-down list of classes related to the entity displayed in a table. Once the user selects the required entity class, a new browser screen is opened, containing the instances of this entity class, related to the entity instances selected in the initial table.

gui relatedEntities

The XML-name of the component: relatedEntities

Related entities are selected considering the user permissions for entities, entity attributes and screens.

By default, the browser screen for the class selected in the drop-down is defined by convention ({entity_name}.browse,{entity_name}.lookup). Optionally, you can define the screen explicitly in the component.

A filter selecting records related to the selected entities is dynamically created in the browser window.

Example of using the component in screen XML-descriptor:

<table id="invoiceTable"
       multiselect="true"
       width="100%">
    <actions>
        <action id="create"/>
        <action id="edit"/>
        <action id="remove"/>
    </actions>

    <buttonsPanel id="buttonsPanel">
        <button id="createBtn"
                action="invoiceTable.create"/>
        <button id="editBtn"
                action="invoiceTable.edit"/>
        <button id="removeBtn"
                action="invoiceTable.remove"/>

        <relatedEntities for="invoiceTable"
                         openType="NEW_TAB">
            <property name="invoiceItems"
                      screen="sales_InvoiceItem.lookup"
                      filterCaption="msg://invoiceItems"/>
        </relatedEntities>
    </buttonsPanel>
    . . .
</table>

The for attribute is required. It contains the table identifier.

The openType="NEW_TAB" attribute sets the opening mode of the lookup windows to new tab. The entity browser is opened in the current tab by default.

The property element enables explicitly defining the related entity displayed in the drop-down.

property attributes:

  • name – the current entity attribute name, referencing the related entity.

  • screen – the identifier of the browser screen that should be opened.

  • filterCaption – the name of the dynamically generated filter.

The exclude attribute enables excluding some of the related entities from the drop-down list. The value of the property is a regular expression matching reference attributes to exclude.

gui relatedEntitiesTable

The platform provides an API for opening related entities screens without the RelatedEntities component: the RelatedEntitiesAPI interface and its implementation RelatedEntitiesBean. The logic is defined with the openRelatedScreen() method which takes the collection of entities from one side of relation, MetaClass of single entity from this collection, and a chosen field to find related entities.

<button id="related"
        caption="Related customer"/>
@UiController("sales_Order.browse")
@UiDescriptor("order-browse.xml")
@LookupComponent("ordersTable")
@LoadDataBeforeShow
public class OrderBrowse extends StandardLookup<Order> {

    @Inject
    private RelatedEntitiesAPI relatedEntitiesAPI;
    @Inject
    private GroupTable<Order> ordersTable;

    @Subscribe("related")
    protected void onRelatedClick(Button.ClickEvent event) {
        relatedEntitiesAPI.openRelatedScreen(ordersTable.getSelected(), Order.class, "customer");
    }

}

By default, the standard entity browser screen is opened. Optionally you can add a RelatedScreenDescriptor parameter to make the method open another screen or open it with parameters. The RelatedScreenDescriptor is a POJO which can store the screen identifier (String), open type (WindowManager.OpenType), filter caption (String), and screen params (Map<String, Object>).

relatedEntitiesAPI.openRelatedScreen(ordersTable.getSelected(),
        Order.class, "customer",
        new RelatedEntitiesAPI.RelatedScreenDescriptor("sales_Customer.lookup", WindowManager.OpenType.DIALOG));

Attributes of relatedEntities

align - caption - captionAsHtml - css - description - descriptionAsHtml - enable - exclude - box.expandRatio - for - icon - id - openType - stylename - tabIndex - visible - width

Attributes of property

caption - filterCaption - name - screen


3.5.2.1.37. ResizableTextArea

ResizableTextArea is a multi-line text editor field with the ability to change the size.

XML-name of the component: resizableTextArea.

ResizableTextArea mostly replicates the functionality of the TextArea component and has the following specific attributes:

  • resizableDirection – defines the way a user can change the size of the component unless the percentage size is set for the component.

    <textArea id="textArea" resizableDirection="BOTH"/>
    gui textField resizable

    There are four resize modes available:

    • BOTH – the component is resizable in both directions. BOTH is the default value. The component is not resizable if the percentage size is set for the component.

    • NONE – the component can not be resized.

    • VERTICAL – the component is resizable only vertically. The component can not be resized vertically if the percentage size is set for the component.

    • HORIZONTAL – the component is resizable only horizontally. The component can not be resized horizontally if the percentage width is set for the component.

    The area size change events can be tracked using the ResizeListener interface. For example:

    resizableTextArea.addResizeListener(resizeEvent ->
            notifications.create()
                    .withCaption("Resized")
                    .show());


3.5.2.1.38. RichTextArea

This is a text area to display and enter formatted text.

XML name of the component: richTextArea

RichTextArea is implemented only for Web Client.

Basically, RichTextArea mirrors the functionality of TextField, except that you cannot set datatype for it. So, RichTextArea is intended for work only with text and entity attributes of type String.

gui RichTextAreaInfo


3.5.2.1.39. SearchPickerField

The SearchPickerField component is used to search for entity instances according to the entered string. A user should enter a few characters and press Enter. If several matches have been found, all of them will be displayed in a drop-down list. If only one instance matches the search query, it immediately becomes a component value. SearchPickerField also enables performing actions by clicking on buttons on the right.

gui searchPickerFieldOverlap

SearchPickerField works only in screens based on legacy API. The similar functionality for the current API is provided by the SuggestionPickerField component.

XML name of the component: searchPickerField.

  • To use SearchPickerField component, you need to create collectionDatasource and specify a query, which contains corresponding search conditions. Condition must contain a parameter named custom$searchString. This parameter will be populated with a substring entered by the user. A data source with a search condition should be defined in the optionsDatasource attribute of the component. For example:

    <dsContext>
        <datasource id="carDs" class="com.company.sample.entity.Car" view="_local"/>
        <collectionDatasource id="colorsDs" class="com.company.sample.entity.Color" view="_local">
            <query>
                select c from sample_Color c
                where c.name like :(?i)custom$searchString
            </query>
        </collectionDatasource>
    </dsContext>
    <layout>
        <searchPickerField datasource="carDs" property="color" optionsDatasource="colorsDs"/>
    </layout>

    In this case, the component will look for instances of Colour entity according to the occurrence of the substring in its name attribute. The (?i) prefix is used for case-insensitive search (see Case-Insensitive Search for a Substring). The selected value will be set in the colour attribute of the Car entity located in the carDs datasource.

    The escapeValueForLike attribute set to true enables searching for the values that contain special symbols %, \, and _ using like-clauses. In order to use escapeValueForLike = true, modify the query of the collection data source adding the escape value to it:

    select c from ref_Colour c
    where c.name like :(?i)custom$searchString or c.description like :(?i)custom$searchString escape '\'

    The escapeValueForLike attribute works for all databases except HSQLDB.

  • Using the minSearchStringLength attribute the minimum number of characters, which the user should enter to search for values, can be defined.

  • In the screen controller, two component methods can be implemented that will be invoked:

    • If the number of entered characters is less than the value of minSearchStringLength attribute.

    • If the search of characters entered by the user has returned no results.

    Below is an example of implementing methods to display messages to the user:

    @Inject
    private Notifications notifications;
    @Inject
    private SearchPickerField colorField;
    
    @Subscribe
    protected void onInit(InitEvent event) {
        colorField.setSearchNotifications(new SearchField.SearchNotifications() {
            @Override
            public void notFoundSuggestions(String filterString) {
                notifications.create()
                        .withCaption("No colors found for search string: " + filterString)
                        .withType(Notifications.NotificationType.TRAY)
                        .show();
            }
    
            @Override
            public void needMinSearchStringLength(String filterString, int minSearchStringLength) {
                notifications.create()
                        .withCaption("Minimum length of search string is " + minSearchStringLength)
                        .withType(Notifications.NotificationType.TRAY)
                        .show();
            }
        });
    }
  • SearchPickerField implements LookupField and PickerField interfaces. Thus, it inherits the same functionality except the default list of actions added when defining the component in XML: for SearchPickerField these are lookup lookupBtn and open openBtn actions.



3.5.2.1.40. SideMenu

SideMenu component provides means of customizing the main window layout, managing menu items, adding icons and badges and applying custom styles.

It can also be used in any screen as any other visual component.

gui sidemenu

XML name of the component: sideMenu.

An example of component definition in an XML-descriptor of a screen:

<sideMenu id="sideMenu"
          width="100%"
          selectOnClick="true"/>

CUBA Studio provides the screen template for main window with the sideMenu component and predefined styles within the side panel:

<layout>
    <hbox id="horizontalWrap"
          expand="workArea"
          height="100%"
          stylename="c-sidemenu-layout"
          width="100%">
        <vbox id="sideMenuPanel"
              expand="sideMenu"
              height="100%"
              margin="false,false,true,false"
              spacing="true"
              stylename="c-sidemenu-panel"
              width="250px">
            <hbox id="appTitleBox"
                  spacing="true"
                  stylename="c-sidemenu-title"
                  width="100%">
                <label id="appTitleLabel"
                       align="MIDDLE_CENTER"
                       value="mainMsg://application.logoLabel"/>
            </hbox>
            <embedded id="logoImage"
                      align="MIDDLE_CENTER"
                      stylename="c-app-icon"
                      type="IMAGE"/>
            <hbox id="userInfoBox"
                  align="MIDDLE_CENTER"
                  expand="userIndicator"
                  margin="true"
                  spacing="true"
                  width="100%">
                <userIndicator id="userIndicator"
                               align="MIDDLE_CENTER"/>
                <newWindowButton id="newWindowButton"
                                 description="mainMsg://newWindowBtnDescription"
                                 icon="app/images/new-window.png"/>
                <logoutButton id="logoutButton"
                              description="mainMsg://logoutBtnDescription"
                              icon="app/images/exit.png"/>
            </hbox>
            <sideMenu id="sideMenu"
                      width="100%"/>
            <ftsField id="ftsField"
                      width="100%"/>
        </vbox>
        <workArea id="workArea"
                  height="100%">
            <initialLayout margin="true"
                           spacing="true">
                <label id="welcomeLabel"
                       align="MIDDLE_CENTER"
                       stylename="c-welcome-text"
                       value="mainMsg://application.welcomeText"/>
            </initialLayout>
        </workArea>
    </hbox>
</layout>

sideMenu attributes:

  • The selectOnClick attribute, when set to true, highlights the selected menu item on mouse click. The default value is false.

gui sidemenu 2

Methods of the SideMenu interface:

  • createMenuItem - creates new menu item, but does not add this item to menu. Id must be unique for whole menu.

  • addMenuItem - adds menu item to the menu.

  • removeMenuItem - removes menu item from the items list.

  • getMenuItem - returns menu item from the menu tree by its id.

  • hasMenuItems - returns true if the menu has items.

SideMenu component is used to display menu items. The MenuItem API enables creating menu items in the screen controller. The methods below can be used for dynamic update of menu items depending on the application business logic. The example of adding a menu item programmatically:

SideMenu.MenuItem item = sideMenu.createMenuItem("special");
item.setCaption("Daily offer");
item.setBadgeText("New");
item.setIconFromSet(CubaIcon.GIFT);
sideMenu.addMenuItem(item,0);
gui sidemenu 3

Methods of the MenuItem interface:

  • setCaption - sets item caption.

  • setCaptionAsHtml - enables or disables HTML mode for caption.

  • setBadgeText - sets badge text for the item. Badges are shown as small widget on the right side of menu items, for example:

    int count = 5;
    SideMenu.MenuItem item = sideMenu.createMenuItem("count");
    item.setCaption("Messages");
    item.setBadgeText(count + " new");
    item.setIconFromSet(CubaIcon.ENVELOPE);
    sideMenu.addMenuItem(item,0);
    gui sidemenu 4

    The badge text can be dynamically updated with the help of the Timer component:

    public void updateCounters(Timer source) {
        sideMenu.getMenuItemNN("sales")
                .setBadgeText(String.valueOf(LocalTime.MIDNIGHT.minusSeconds(timerCounter-source.getDelay())));
        timerCounter++;
    }
    gui sidemenu 5
  • setIcon - sets menu icon.

  • setCommand - sets item command, or the action to be performed on this menu item click.

  • addChildItem/removeChildItem - adds/removes menu item to the children list.

  • setExpanded - expands or collapses sub-menu with children by default.

  • setStyleName - sets one or more user-defined style names of the component, replacing any previous user-defined styles. Multiple styles can be specified as a space-separated list of style names. The style names must be valid CSS class names.

    The standard sideMenu template includes several predefined styles: c-sidemenu-layout, c-sidemenu-panel and c-sidemenu-title. The default c-sidemenu style is supported in the Halo and Hover themes and their extensions.

  • setTestId - sets cuba-id value for UI testing.

The appearance of the SideMenu component can be customized using SCSS variables with $cuba-sidemenu-* and $cuba-responsive-sidemenu-* prefixes. You can change these variables in the visual editor after creating a theme extension or a custom theme.



3.5.2.1.41. SourceCodeEditor

SourceCodeEditor is designed to display and enter source code. It is a multi-line text area featured with code highlighting and optional print margin and gutter with line numbers.

XML-name of the component: sourceCodeEditor.

SourceCodeEditor is implemented for Web Client.

Basically, SourceCodeEditor mostly replicates the functionality of the TextField component and has the following specific attributes:

  • if handleTabKey is true, the Tab button on the keyboard is handled to indent lines, when false, it is used to advance the cursor or focus to the next tab stop. This attribute should be set when the screen is initialized and will not work if changed at a runtime.

All following properties can be easily changed at a runtime:

  • highlightActiveLine is used to highlight the line the caret is on.

  • mode provides the list of languages supported for the syntax highlight. This list is defined in the Mode enumeration of the SourceCodeEditor interface and includes the following languages: Java, HTML, XML, Groovy, SQL, JavaScript, Properties, and Text with no highlight.

  • printMargin attribute sets if the printing edge line should be displayed or hidden.

  • showGutter is used to hide or show the left gutter with line numbers.

Below is an example of SourceCodeEditor component adjustable at a runtime.

XML-descriptor:

<hbox spacing="true">
    <checkBox id="highlightActiveLineCheck" align="BOTTOM_LEFT" caption="Highlight Active Line"/>
    <checkBox id="printMarginCheck" align="BOTTOM_LEFT" caption="Print Margin"/>
    <checkBox id="showGutterCheck" align="BOTTOM_LEFT" caption="Show Gutter"/>
    <lookupField id="modeField" align="BOTTOM_LEFT" caption="Mode" required="true"/>
</hbox>
<sourceCodeEditor id="simpleCodeEditor" width="100%"/>

The controller:

@Inject
private CheckBox highlightActiveLineCheck;
@Inject
private LookupField<HighlightMode> modeField;
@Inject
private CheckBox printMarginCheck;
@Inject
private CheckBox showGutterCheck;
@Inject
private SourceCodeEditor simpleCodeEditor;

@Subscribe
protected void onInit(InitEvent event) {
    highlightActiveLineCheck.setValue(simpleCodeEditor.isHighlightActiveLine());
    highlightActiveLineCheck.addValueChangeListener(e ->
            simpleCodeEditor.setHighlightActiveLine(Boolean.TRUE.equals(e.getValue())));

    printMarginCheck.setValue(simpleCodeEditor.isShowPrintMargin());
    printMarginCheck.addValueChangeListener(e ->
            simpleCodeEditor.setShowPrintMargin(Boolean.TRUE.equals(e.getValue())));

    showGutterCheck.setValue(simpleCodeEditor.isShowGutter());
    showGutterCheck.addValueChangeListener(e ->
            simpleCodeEditor.setShowGutter(Boolean.TRUE.equals(e.getValue())));

    Map<String, HighlightMode> modes = new HashMap<>();
    for (HighlightMode mode : SourceCodeEditor.Mode.values()) {
        modes.put(mode.toString(), mode);
    }

    modeField.setOptionsMap(modes);
    modeField.setValue(HighlightMode.TEXT);
    modeField.addValueChangeListener(e ->
            simpleCodeEditor.setMode(e.getValue()));
}

The result will be:

gui SourceCodeEditor 1

SourceCodeEditor can also support code autocomplete provided by the Suggester class. To activate word completion, the setSuggester method should be invoked, for example:

@Inject
protected DataGrid<User> usersGrid;
@Inject
private SourceCodeEditor suggesterCodeEditor;
@Inject
private CollectionContainer<User> usersDc;
@Inject
private CollectionLoader<User> usersDl;

@Subscribe
protected void onInit(InitEvent event) {
    suggesterCodeEditor.setSuggester((source, text, cursorPosition) -> {
        List<Suggestion> suggestions = new ArrayList<>();
        usersDl.load();
        for (User user : usersDc.getItems()) {
            suggestions.add(new Suggestion(source, user.getLogin(), user.getName(), null, -1, -1));
        }
        return suggestions;
    });
}

The result:

gui SourceCodeEditor 2

The appearance of the SourceCodeEditor component can be customized using SCSS variables with $cuba-sourcecodeeditor-* prefix. You can change these variables in the visual editor after creating a theme extension or a custom theme.



3.5.2.1.42. SuggestionField

The SuggestionField component is designed to search for certain values according to a string entered by a user. It differs from SuggestionPickerField in that it can use any types of options: for instance, entities, strings, or enum values, and does not have action buttons. The list of options is loaded in background according to the logic defined by the application developer on the server side.

gui suggestionField 1

XML name of the component: suggestionField.

suggestionField attributes:

  • asyncSearchDelayMs - sets the delay between the last key press action and asynchronous search.

  • minSearchStringLength - sets the minimal string length which is required to perform suggestions search.

  • popupWidth - sets the width of the suggestion popup.

    Possible options:

    • auto - the popup width will be equal to the maximum width of suggestions,

    • parent - the popup width will be equal to the width of main component,

    • absolute (e.g. "170px") or relative (e.g. "50%") value.

  • suggestionsLimit - sets the limit of suggestions to be displayed.

suggestionField elements:

  • query - an optional element which enables defining a query for selecting suggested values. The query element, in turn, has the following attributes:

    • entityClass (required) - full qualified name of entity class.

    • view - optional attribute that specifies the view to be used for loading the queried entity.

    • escapeValueForLike - enables searching for the values that contain special symbols: %, \, etc. Default value is false.

    • searchStringFormat - a Groovy string, thus you can use any valid Groovy-string expressions.

    <suggestionField id="suggestionField"
                     captionProperty="login">
        <query entityClass="com.haulmont.cuba.security.entity.User"
               escapeValueForLike="true"
               view="user.edit"
               searchStringFormat="%$searchString%">
            select e from sec$User e where e.login like :searchString escape '\'
        </query>
    </suggestionField>

    If the query is not defined, the list of options must be provided by SearchExecutor, assigned programmatically (see below).

In the most common case, it is sufficient to set SearchExecutor to the component. SearchExecutor is a functional interface that contains a single method: List<E> search(String searchString, Map<String, Object> searchParams):

suggestionField.setSearchExecutor((searchString, searchParams) -> {
    return Arrays.asList(entity1, entity2, ...);
});

SearchExecutor can return any types of options, for example, entities, strings, or enum values.

  • Entities:

customersDs.refresh();
List<Customer> customers = new ArrayList<>(customersDs.getItems());
suggestionField.setSearchExecutor((searchString, searchParams) ->
        customers.stream()
                .filter(customer -> StringUtils.containsIgnoreCase(customer.getName(), searchString))
                .collect(Collectors.toList()));
  • Strings:

List<String> strings = Arrays.asList("Red", "Green", "Blue", "Cyan", "Magenta", "Yellow");
stringSuggestionField.setSearchExecutor((searchString, searchParams) ->
        strings.stream()
                .filter(str -> StringUtils.containsIgnoreCase(str, searchString))
                .collect(Collectors.toList()));
  • Enum:

List<SendingStatus> enums = Arrays.asList(SendingStatus.values());
enumSuggestionField.setSearchExecutor((searchString, searchParams) ->
        enums.stream()
                .map(sendingStatus -> messages.getMessage(sendingStatus))
                .filter(str -> StringUtils.containsIgnoreCase(str, searchString))
                .collect(Collectors.toList()));
  • OptionWrapper class is used when you need to separate a value of any type and its string representation:

List<OptionWrapper> wrappers = Arrays.asList(
        new OptionWrapper("One", 1),
        new OptionWrapper("Two", 2),
        new OptionWrapper("Three", 3);
suggestionField.setSearchExecutor((searchString, searchParams) ->
        wrappers.stream()
                .filter(optionWrapper -> StringUtils.containsIgnoreCase(optionWrapper.getCaption(), searchString))
                .collect(Collectors.toList()));

The search() method is executed in a background thread so it cannot access visual components or datasources used by visual components. Call DataManager or a middleware service directly; or process and return data loaded to the screen beforehand.

The searchString parameter can be used to filter candidates using the string entered by the user. You can use the escapeForLike() method to search for the values that contain special symbols:

suggestionField.setSearchExecutor((searchString, searchParams) -> {
    searchString = QueryUtils.escapeForLike(searchString);
    return dataManager.loadList(LoadContext.create(Customer.class).setQuery(
            LoadContext.createQuery("select c from sample_Customer c where c.name like :name order by c.name escape '\\'")
                .setParameter("name", "%" + searchString + "%")));
});
  • OptionsStyleProvider enables you to use separate style names for the suggested options displayed by suggestionField:

    suggestionField.setOptionsStyleProvider((field, item) -> {
        User user = (User) item;
        switch (user.getGroup().getName()) {
            case "Company":
                return "company";
            case "Premium":
                return "premium";
            default:
                return "company";
        }
    });

The appearance of the SuggestionField component can be customized using SCSS variables with $cuba-suggestionfield-* prefix. You can change these variables in the visual editor after creating a theme extension or a custom theme.



3.5.2.1.43. SuggestionPickerField

The SuggestionPickerField component is designed to search for entity instances according to a string entered by a user. It differs from SearchPickerField in that it refreshes the list of options on each entered symbol without the need to press Enter. The list of options is loaded in background according to the logic defined by the application developer on the server side.

SuggestionPickerField is also a PickerField and can contain actions represented by buttons on the right.

gui suggestionPickerField 1

XML name of the component: suggestionPickerField.

SuggestionPickerField is used to select reference entity attributes, so you usually set its dataContainer and property attributes:

<data>
    <instance id="orderDc"
              class="com.company.sales.entity.Order"
              view="order-with-customer">
        <loader id="orderDl"/>
    </instance>
</data>
<layout>
<suggestionPickerField id="suggestionPickerField"
                       captionProperty="name"
                       dataContainer="orderDc"
                       property="customer"/>
</layout>

suggestionPickerField attributes:

  • asyncSearchDelayMs - sets the delay between the last key press action and asynchronous search.

  • metaClass - sets the link to the component’s MetaClass if the component is used without binding to a data component, i.e., without setting dataContainer and property.

  • minSearchStringLength - sets the minimal string length which is required to perform suggestions search.

  • popupWidth - sets the width of the suggestion popup.

    Possible options:

    • auto - the popup width will be equal to the maximum width of suggestions,

    • parent - the popup width will be equal to the width of main component,

    • absolute (e.g. "170px") or relative (e.g. "50%") value.

  • suggestionsLimit - sets the limit of suggestions to be displayed.

The appearance of suggestionPickerField and its popup can be customized using the stylename attribute. The stylename for the popup should have the same name as for the main component: for example, if the component has custom stylename "my-awesome-stylename", its popup should have the stylename "c-suggestionfield-popup my-awesome-stylename".

suggestionPickerField elements:

  • actions - an optional element describing the actions related to the component. In addition to custom arbitrary actions, suggestionPickerField supports the following standard PickerField actions: picker_lookup, picker_clear, picker_open.

Base SuggestionPickerField usage

In the most common case, it is sufficient to set SearchExecutor to the component. SearchExecutor is a functional interface that contains a single method: List<E extends Entity> search(String searchString, Map<String, Object> searchParams):

suggestionPickerField.setSearchExecutor((searchString, searchParams) -> {
    return Arrays.asList(entity1, entity2, ...);
});

The search() method is executed in a background thread so it cannot access visual components or datasources used by visual components. Call DataManager or a middleware service directly; or process and return data loaded to the screen beforehand.

The searchString parameter can be used to filter candidates using the string entered by the user. You can use the escapeForLike() method to search for the values that contain special symbols:

suggestionPickerField.setSearchExecutor((searchString, searchParams) -> {
    searchString = QueryUtils.escapeForLike(searchString);
    return dataManager.loadList(LoadContext.create(Customer.class).setQuery(
            LoadContext.createQuery("select c from sample_Customer c where c.name like :name order by c.name escape '\\'")
                .setParameter("name", "%" + searchString + "%")));
});
ParametrizedSearchExecutor usage

In the previous examples, searchParams is an empty map. To define params, you should use ParametrizedSearchExecutor:

suggestionPickerField.setSearchExecutor(new SuggestionField.ParametrizedSearchExecutor<Customer>(){
    @Override
    public Map<String, Object> getParams() {
        return ParamsMap.of(...);
    }

    @Override
    public List<Customer> search(String searchString, Map<String, Object> searchParams) {
        return executeSearch(searchString, searchParams);
    }
});
EnterActionHandler and ArrowDownActionHandler usage

Another way to use the component is to set EnterActionHandler or ArrowDownActionHandler. These listeners are fired when a user presses Enter or Arrow Down keys when the popup with suggestions is hidden. They are also functional interfaces with single method and single parameter - currentSearchString. You can set up your own action handlers and use SuggestionField.showSuggestions() method which accepts the list of entities to show suggestions.

suggestionPickerField.setArrowDownActionHandler(currentSearchString -> {
    List<Customer> suggestions = findSuggestions();
    suggestionPickerField.showSuggestions(suggestions);
});

suggestionPickerField.setEnterActionHandler(currentSearchString -> {
    List<Customer> suggestions = getDefaultSuggestions();
    suggestionPickerField.showSuggestions(suggestions);
});


3.5.2.1.44. Table

The Table component presents information in a table view, sorts data, manages table columns and headers and invokes actions for selected rows.

gui table

XML-name of the component: table

An example of component definition in an XML-descriptor of a screen:

<data readOnly="true">
    <collection id="ordersDc" class="com.company.sales.entity.Order" view="order-with-customer">
        <loader id="ordersDl">
            <query>
                <![CDATA[select e from sales_Order e]]>
            </query>
        </loader>
    </collection>
</data>
<layout>
<table id="ordersTable" dataContainer="ordersDc" width="100%">
    <columns>
        <column id="date"/>
        <column id="amount"/>
        <column id="customer"/>
    </columns>
    <rowsCount/>
</table>
</layout>

In the example, the data element defines the collection container, which selects Order entities using JPQL query. The table element defines the data container, while columns element defines which entity attributes are used as table columns.

table elements:

  • rows – a required element if the datasource attribute is used for data binding.

    Each row can have an icon in an additional column on the left. Create an implementation of the ListComponent.IconProvider interface in the screen controller and set it for the table:

    @Inject
    private Table<Customer> table;
    
    @Subscribe
    protected void onInit(InitEvent event) {
        table.setIconProvider(new ListComponent.IconProvider<Customer>() {
            @Nullable
            @Override
            public String getItemIcon(Customer entity) {
                CustomerGrade grade = entity.getGrade();
                switch (grade) {
                    case PREMIUM: return "icons/premium_grade.png";
                    case HIGH: return "icons/high_grade.png";
                    case MEDIUM: return "icons/medium_grade.png";
                    default: return null;
                }
            }
        });
    }
  • columns – a required element defining the set of columns for a table. Element columns has the following attributes:

    • includeAll – load all the attributes from the view that is defined in dataContainer or datasource.

      In the example below, we will show all the attributes from the view used in the customersDc. If the view contains system properties, they will be shown too.

      <table id="table"
             width="100%"
             height="100%"
             dataContainer="customersDc">
          <columns includeAll="true"/>
      </table>

      If the view of the entity contains a reference attribute, this attribute will be displayed according to its instance name. If you want to show a specific attribute, it must be defined in the view as well as in the column element:

      <columns includeAll="true">
          <column id="address.street"/>
      </columns>

      If no view is specified, includeAll attribute will load all the attributes from a given entity and its ancestors.

    • exclude – comma-separated list of attributes that should not be loaded to the table.

      In the example below, we will show all the attributes excluding name and order:

      <table id="table"
             width="100%"
             height="100%"
             dataContainer="customersDc">
          <columns includeAll="true"
                   exclude="name, order"/>
      </table>

    Each column is described in a nested column element with the following attributes:

    • id − a mandatory attribute, contains the name of an entity attribute displayed in the column. Can be either an attribute of the entity from the data container or a linked entity – object graph traversal is indicated with a dot. For example:

      <columns>
          <column id="date"/>
          <column id="customer"/>
          <column id="customer.name"/>
          <column id="customer.address.country"/>
      </columns>
    • caption − an optional attribute containing the column caption. If not specified, a localized attribute name will be displayed.

    • captionAsHtml − an optional attribute defining whether HTML tags can be used in the column caption. Default value is false.

      <column id="name"
              caption="msg://role.name"
              captionAsHtml="true"/>
      role.name=<em>Name</em>
    • collapsed − an optional attribute; hides the column by default when set to true. Users can control column’s visibility using the menu available via a gui_table_columnControl button in the top right part of the table when the table’s columnControlVisible attribute is not false. By default, collapsed is false.

    • expandRatio − an optional attribute that specifies the expand ratio for each column. The ratio must be greater than or equal to 0. If some value is set for at least one column, all implicit values are ignored, and only explicitly assigned values are considered. If you set width and expandRatio attributes simultaneously, it will cause an error in the application.

    • width − an optional attribute controlling default column width. May contain only numeric values in pixels.

    • align − an optional attribute that sets text alignment of column cells. Possible values: LEFT, RIGHT, CENTER. Default is LEFT.

    • editable − an optional attribute allowing editing of the corresponding column in the table. In order for a column to be editable, the editable attribute of the entire table should be set to true as well. Changing this property at runtime is not supported.

    • sortable − an optional attribute to disable sorting of the column. Takes effect if the whole table has sortable attribute set to true (which is by default).

    • maxTextLength – an optional attribute allowing to limit the number of characters in a cell. If the difference between the actual and the maximum allowed number of characters does not exceed the 10 character threshold, the "extra" characters remain unhidden. To see the entire record, users need to click on its visible part. An example of a column with a 10 character limitation:

      gui table column maxTextLength
    • linkScreen - contains the identifier of the screen that is opened by clicking the link enabled in the link attribute.

    • linkScreenOpenType - sets the screen opening mode (THIS_TAB, NEW_TAB or DIALOG).

    • linkInvoke - invokes the controller method instead of opening the screen.

      @Inject
      private Notifications notifications;
      
      public void linkedMethod(Entity item, String columnId) {
          Customer customer = (Customer) item;
          notifications.create()
                  .withCaption(customer.getName())
                  .show();
      }
    • captionProperty - the name of an entity attribute which should be shown in the column instead of specified by id. For example, if you have a reference to the Priority entity with the name and orderNo attributes, you can define the following column:

      <column id="priority.orderNo" captionProperty="priority.name" caption="msg://priority" />

      In this case, the column will display the priority name, but the sorting of the column will be done by the priority order.

    • an optional generator attribute contains the link to a method in the screen controller that creates a visual component to be shown in table cells:

      <columns>
          <column id="name"/>
          <column id="imageFile"
                  generator="generateImageFileCell"/>
      </columns>
      public Component generateImageFileCell(Employee entity){
          Image image = uiComponents.create(Image.NAME);
          image.setSource(FileDescriptorResource.class).setFileDescriptor(entity.getImageFile());
          return image;
      }

      It can be used instead of providing an implementation of Table.ColumnGenerator to the addGeneratedColumn() method.

    • column element may contain a nested formatter element that allows you to represent the attribute value in a format different from the standard for this Datatype:

      <column id="date">
          <formatter class="com.haulmont.cuba.gui.components.formatters.DateFormatter"
                     format="yyyy-MM-dd HH:mm:ss"
                     useUserTimezone="true"/>
      </column>
  • rowsCount − an optional element adding the RowsCount component for the table; this component enables loading the table data in pages. Page size can be defined by limiting the number of records in the data container using the loader’s setMaxResults() method. Typically, this is performed by a Filter component linked to the table’s data loader. However, if there is no generic filter, this method can be called directly from the screen controller.

    RowsCount component can also show the total number of records returned by the current query from the container without extracting the records themselves. It invokes com.haulmont.cuba.core.global.DataManager#getCount when user clicks the ? icon, which results in performing a database query with the same conditions as the current query, but using a COUNT(*) aggregate function instead. The number retrieved is displayed instead of the ? icon.

  • actions − an optional element describing the actions, related to the table. In addition to custom arbitrary actions, the element supports the following standard actions, defined in the com.haulmont.cuba.gui.actions.list package: create, edit, remove, refresh, add, exclude, excel.

  • buttonsPanel – an optional element, which adds a ButtonsPanel container to show action buttons above the table.

table attributes:

  • multiselect attribute enables setting multiple selection mode for table rows. If multiselect is true, users can select multiple rows in the table using keyboard or mouse holding Ctrl or Shift keys. By default, multiple selection mode is switched off.

  • sortable attribute enables sorting data in the table. By default, it is set to true. If sorting is allowed, clicking a column header will show a gui_sortable_down/gui_sortable_up icon to the right of the column name. You can disable sorting for a particular column by using its sortable attribute.

    Table sorting can be performed differently depending on whether all the records can be placed on one page or not. If they can, sorting is performed in memory without database queries. If there is more than one page, sorting is performed in the database by sending a new query with the corresponding ORDER BY condition.

    A table column may refer to a local attribute or a linked entity. For example:

    <table id="ordersTable"
           dataContainer="ordersDc">
        <columns>
            <column id="customer.name"/> <!-- the 'name' attribute of the 'Customer' entity -->
            <column id="contract"/>      <!-- the 'Contract' entity -->
        </columns>
    </table>

    In the latter case, the database sorting will be performed by attributes defined in the @NamePattern annotation of the related entity. If the entity has no such annotation, the sorting will be performed in memory only within the current page.

    If the column refers to a non-persistent entity attribute, the database sorting will be performed by attributes defined in the related() parameter of the @MetaProperty annotation. If no related attributes are specified, the sorting will be performed in memory only within the current page.

    If the table is connected to a nested property container that contains a collection of related entities, the collection attribute must be of ordered type (List or LinkedHashSet) for table to be sortable. If the attribute is of type Set, the sortable attribute has no effect and the table cannot be sorted by users.

    You can provide your own implementation of sorting if needed.

  • presentations attribute controls the mechanism of presentations. By default, the value is false. If the attribute value is true, a corresponding icon is added to the top right corner of the table gui_presentation. The mechanism of presentations is implemented for the Web Client only.

  • If the columnControlVisible attribute is set to false, users cannot hide columns using the drop-down menu of the gui_table_columnControl button in the right part of the table header. Currently displayed columns are marked with checkmarks in the menu.

gui table columnControl all
  • If the reorderingAllowed attribute is set to false, users cannot change columns order by dragging them with a mouse.

  • If the columnHeaderVisible attribute is set to false, the table has no header.

  • If the showSelection attribute is set to false, a current row is not highlighted.

  • contextMenuEnabled attribute enables the context menu. By default this attribute is set to true. The context menu shows table actions (if any) and the System Information item containing information on the selected entity (if the user has cuba.gui.showInfo permission).

  • Setting multiLineCells to true enables multi-line display for cells containing several lines of text. In this mode, the web browser will load all the rows of the current table page at once, instead of lazy-loading the visible part of the table. It is required for proper scrolling in the Web Client. The default value is false.

  • aggregatable attribute enables aggregation for table rows. The following operations are supported:

    • SUM – calculate the sum

    • AVG – find the average value

    • COUNT – calculate the total number

    • MIN – find the minimum value

    • MAX – find the maximum value

    The aggregation element should be set for aggregated table columns with the type attribute, which sets the aggregation function. By default, only numeric data types are supported in aggregated columns, such as Integer, Double, Long and BigDecimal. The aggregated table values are shown in an additional row at the top of the table. An example of an aggregated table description:

    <table id="itemsTable" aggregatable="true" dataContainer="itemsDc">
        <columns>
            <column id="product"/>
            <column id="quantity"/>
            <column id="amount">
                <aggregation type="SUM"/>
            </column>
        </columns>
    </table>

    The aggregation element can contain the editable attribute. Setting the attribute to true in conjunction with using the setAggregationDistributionProvider() method allows developers to implement algorithms for the distribution of data between the rows of the table.

    The aggregation element can also contain the strategyClass attribute specifying a class implementing the AggregationStrategy interface (see below the example of setting an aggregation strategy programmatically).

    The valueDescription attribute defines a hint which is displayed in a popup when a user hovers the mouse cursor on the aggregated value. For the operations listed above (SUM, AVG, COUNT, MIN, MAX), popup hints are already available by default.

    A Formatter can be specified to display the aggregated value in the format other than the standard for this Datatype:

    <column id="amount">
        <aggregation type="SUM">
            <formatter class="com.company.sample.MyFormatter"/>
        </aggregation>
    </column>

    The aggregationStyle attribute allows you to specify the location of the aggregation row: TOP or BOTTOM. TOP is used by default.

    In addition to the operations listed above, you can define a custom aggregation strategy by implementing the AggregationStrategy interface and passing it to the setAggregation() method of the Table.Column class inside the AggregationInfo instance. For example:

    public class TimeEntryAggregation implements AggregationStrategy<List<TimeEntry>, String> {
        @Override
        public String aggregate(Collection<List<TimeEntry>> propertyValues) {
            HoursAndMinutes total = new HoursAndMinutes();
            for (List<TimeEntry> list : propertyValues) {
                for (TimeEntry timeEntry : list) {
                    total.add(HoursAndMinutes.fromTimeEntry(timeEntry));
                }
            }
            return StringFormatHelper.getTotalDayAggregationString(total);
        }
        @Override
        public Class<String> getResultClass() {
            return String.class;
        }
    }
    AggregationInfo info = new AggregationInfo();
    info.setPropertyPath(metaPropertyPath);
    info.setStrategy(new TimeEntryAggregation());
    
    Table.Column column = weeklyReportsTable.getColumn(columnId);
    column.setAggregation(info);
  • editable attribute enables switching the table to in-place editing mode. In this mode, the columns with editable = true attribute show components to edit the attributes of the corresponding entity.

    The component type for each editable column is selected automatically based on the type of the corresponding entity attribute. For example, for string and numeric attributes, the application will use TextField, for DateDateField, for lists – LookupField, for links to other entities – PickerField.

    For a Date type editable column, you can additionally define dateFormat or resolution attributes similar to the ones described for the DateField.

    optionsContainer and captionProperty attributes can be additionally defined for an editable column showing a linked entity. If optionsContainer is set, the application will use LookupField instead of PickerField.

    Custom configuration (including editing) of a cell can be performed using Table.addGeneratedColumn() method – see below.

  • In Web Client with a Halo-based theme, stylename attribute enables setting predefined styles to the Table component either in the XML descriptor or in the screen controller:

    <table id="table"
           dataContainer="itemsDc"
           stylename="no-stripes">
        <columns>
            <column id="product"/>
            <column id="quantity"/>
        </columns>
    </table>

    When setting a style programmatically, select one of the HaloTheme class constants with the TABLE_ prefix:

    table.setStyleName(HaloTheme.TABLE_NO_STRIPES);

    Table styles:

    • borderless - removes the outer border of the table.

    • compact - reduces the white space inside the table cells.

    • no-header - hides the table column headers.

    • no-horizontal-lines - removes the horizontal divider lines between the table rows.

    • no-stripes - removes the alternating row colors.

    • no-vertical-lines - removes the vertical divider lines between the table columns.

    • small - small font size and reduced the white space inside the table cells.

Methods of the Table interface:

  • addColumnCollapsedListener() method is useful for tracking the columns visibility with the help of the ColumnCollapsedListener interface implementation.

  • getSelected(), getSingleSelected() return instances of the entities corresponding to the selected rows of the table. A collection can be obtained by invoking getSelected(). If nothing is selected, the application returns an empty set. If multiselect is disabled, it is more convenient to use getSingleSelected() method returning one selected entity or null, if nothing is selected.

  • addSelectionListener() enables tracking the table rows selection. For example:

    customersTable.addSelectionListener(customerSelectionEvent ->
            notifications.create()
                    .withCaption("You selected " + customerSelectionEvent.getSelected().size() + " customers")
                    .show());

    You can also implement selection tracking by subscription to the corresponding event:

    @Subscribe("customersTable")
    protected void onCustomersTableSelection(Table.SelectionEvent<Customer> event) {
        notifications.create()
                .withCaption("You selected " + customerSelectionEvent.getSelected().size() + " customers")
                .show();
    }

    The origin of the SelectionEvent can be tracked using isUserOriginated() method.

  • addGeneratedColumn() method allows you to define custom representation of data in a column. It takes two parameters: identifier of the column and an implementation of the Table.ColumnGenerator interface. Identifier can match one of the identifiers set for table columns in XML-descriptor – in this case the new column is inserted instead of the one defined in XML. If the identifier does not match any of the columns, a new column is added to the right.

    generateCell() method of the Table.ColumnGenerator interface is invoked for each row of the table. The method receives an instance of the entity displayed in the corresponding row. generateCell() method should return a visual component which will be displayed in the cell.

    Example of using the component:

    @Inject
    private GroupTable<Car> carsTable;
    @Inject
    private CollectionContainer<Car> carsDc;
    @Inject
    private CollectionContainer<Color> colorsDc;
    @Inject
    private UiComponents uiComponents;
    @Inject
    private Actions actions;
    
    @Subscribe
    protected void onInit(InitEvent event) {
        carsTable.addGeneratedColumn("color", entity -> {
            LookupPickerField<Color> field = uiComponents.create(LookupPickerField.NAME);
            field.setValueSource(new ContainerValueSource<>(carsTable.getInstanceContainer(entity), "color"));
            field.setOptions(new ContainerOptions<>(colorsDc));
            field.addAction(actions.create(LookupAction.class));
            field.addAction(actions.create(OpenAction.class));
            return field;
        });
    }

    In the example above, all cells within the color column in the table show the LookupPickerField component. The component will save its value into the color attribute of the entity instance which is displayed in the corresponding row.

    The getInstanceContainer() method returning container with the current entity should be used only for data binding of components created when generating table cells.

    If you want to display just dynamic text, use special class Table.PlainTextCell instead of the Label component. It will simplify rendering and make the table faster.

    If addGeneratedColumn() method receives the identifier of a column which is not declared in XML-descriptor, the header for the new column to be set as follows:

    carsTable.getColumn("colour").setCaption("Colour");

    Consider also using a more declarative approach with the generator XML attribute.

  • requestFocus() method allows you to set focus on the concrete editable field of a certain row. It takes two parameters: an entity instance which identifies the row and the column identifier. Example of requesting a focus:

    table.requestFocus(item, "count");
  • scrollTo() method allows you to scroll table to the concrete row. It takes one parameter: an entity instance identifying the row.

    Example of scrolling:

    table.scrollTo(item);
  • setCellClickListener() method can save you from adding generated columns with components when you need to draw something in cells and receive notifications when a user clicks inside these cells. The CellClickListener implementation passed to this method receives the selected entity and the column identifier. The cells content will be wrapped in span element with cuba-table-clickable-cell style which can be used to specify the cell representation.

    Example of using CellClickListener:

    @Inject
    private Table<Customer> customersTable;
    @Inject
    private Notifications notifications;
    
    @Subscribe
    protected void onInit(InitEvent event) {
        customersTable.setCellClickListener("name", customerCellClickEvent ->
                notifications.create()
                        .withCaption(customerCellClickEvent.getItem().getName())
                        .show());
    }
  • You can use the setAggregationDistributionProvider() method to specify the AggregationDistributionProvider that defines the rules for distributing the aggregated value between rows in a table. If the user enters a value in an aggregated cell, it is distributed to the constituent cells according to a custom algorithm. Supported only for the TOP aggregation style. To make aggregated cells editable, use the editable attribute of the aggregation element.

    When creating a provider, you should use the AggregationDistributionContext<E> object, which contains the data needed to distribute the aggregated value:

    • Column column where total or group aggregation was changed;

    • Object value − the new aggregation value;

    • Collection<E> scope − a collection of entities that will be affected by changed aggregation;

    • boolean isTotalAggregation shows total aggregation changed, or it was group aggregation.

      As an example, consider a table that represents a budget. The user creates budget categories and sets for each of them the percentages according to which the amount of income should be distributed. Next, the user sets the total amount of income in the aggregated cell; after that, it distributes by category.

      An example of table configuration in an XML-descriptor of a screen:

      <table id="budgetItemsTable"
             width="100%"
             dataContainer="budgetItemsDc"
             aggregatable="true"
             editable="true"
             showTotalAggregation="true">
              ...
          <columns>
              <column id="category"/>
              <column id="percent"/>
              <column id="sum">
                  <aggregation editable="true"
                               type="SUM"/>
              </column>
          </columns>
              ...
      </table>

      An example in a screen controller:

      budgetItemsTable.setAggregationDistributionProvider(context -> {
          Collection<BudgetItem> scope = context.getScope();
          if (scope.isEmpty()) {
              return;
          }
      
          double value = context.getValue() != null ?
                  ((double) context.getValue()) : 0;
      
          for (BudgetItem budgetItem : scope) {
              budgetItem.setSum(value / 100 * budgetItem.getPercent());
          }
      });
  • The getAggregationResults() method returns a map with aggregation results, where map keys are table column identifiers, and values are aggregation values.

  • The setStyleProvider() method enables setting table cell display style. The method accepts an implementation of Table.StyleProvider interface as a parameter. getStyleName() method of this interface is invoked by the table for each row and each cell separately. If the method is invoked for a row, the first parameter contains the entity instance displayed by the row, the second parameter is null. If the method is called for a cell, the second parameter contains the name of the attribute displayed by the cell.

    Example of setting a style:

    @Inject
    protected Table customersTable;
    
    @Subscribe
    protected void onInit(InitEvent event) {
        customersTable.setStyleProvider((customer, property) -> {
            if (property == null) {
            // style for row
            if (hasComplaints(customer)) {
                return "unsatisfied-customer";
            }
        } else if (property.equals("grade")) {
            // style for column "grade"
            switch (customer.getGrade()) {
                case PREMIUM: return "premium-grade";
                case HIGH: return "high-grade";
                case MEDIUM: return "medium-grade";
                default: return null;
            }
        }
            return null;
        });
    }

    Then the cell and row styles set in the application theme should be defined. Detailed information on creating a theme is available in Themes. For web client, new styles are defined in the styles.scss file. Style names defined in the controller, together with prefixes identifying table row and column form CSS selectors. For example:

    .v-table-row.unsatisfied-customer {
      font-weight: bold;
    }
    .v-table-cell-content.premium-grade {
      background-color: red;
    }
    .v-table-cell-content.high-grade {
      background-color: green;
    }
    .v-table-cell-content.medium-grade {
      background-color: blue;
    }
  • addPrintable() method enables setting a custom presentation of the data within a column when exporting to an XLS file via the excel standard action or directly using the ExcelExporter class. The method accepts the column identifier and an implementation of the Table.Printable interface for the column. For example:

    ordersTable.addPrintable("customer", new Table.Printable<Customer, String>() {
        @Override
        public String getValue(Customer customer) {
            return "Name: " + customer.getName;
        }
    });

    getValue() method of the Table.Printable interface should return data to be displayed in the table cell. This is not necessarily a string – the method may return values of other types, for example, numeric data or dates, which will be represented in the XLS file accordingly.

    If formatted output to XLS is required for a generated column, an implementation of the Table.PrintableColumnGenerator interface passed to the addGeneratedColumn() method should be used. The value for a cell in an XLS document is defined in the getValue() method of this interface:

    ordersTable.addGeneratedColumn("product", new Table.PrintableColumnGenerator<Order, String>() {
        @Override
        public Component generateCell(Order entity) {
            Label label = uiComponents.create(Label.NAME);
            Product product = order.getProduct();
            label.setValue(product.getName() + ", " + product.getCost());
            return label;
        }
    
        @Override
        public String getValue(Order entity) {
            Product product = order.getProduct();
            return product.getName() + ", " + product.getCost();
        }
    });

    If Printable presentation is not defined for a generated column in one way or another, then the column will either show the value of corresponding entity attribute or nothing if there is no associated entity attribute.

  • The setItemClickAction() method allows you to define an action that will be performed when a table row is double-clicked. If such action is not defined, the table will attempt to find an appropriate one in the list of its actions in the following order:

    • The action assigned to the Enter key by the shortcut property

    • The edit action

    • The view action

      If such action is found, and has enabled = true property, the action is executed.

  • The setEnterPressAction() allows you to define an action executed when Enter is pressed. If such action is not defined, the table will attempt to find an appropriate one in the list of its actions in the following order:

    • The action defined by the setItemClickAction() method

    • The action assigned to the Enter key by the shortcut property

    • The edit action

    • The view action

    If such action is found, and has enabled = true property, the action is executed.

The appearance of the Table component can be customized using SCSS variables with $cuba-table-* prefix. You can change these variables in the visual editor after creating a theme extension or a custom theme.



3.5.2.1.45. TextArea

TextArea is a multi-line text editor field.

XML-name of the component: textArea.

TextArea mostly replicates the functionality of the TextField component and has the following specific attributes:

  • cols and rows set the number of columns and rows of text:

    <textArea id="textArea" cols="20" rows="5" caption="msg://name"/>

    The values of width and height have priority over the values of cols and rows.

  • wordWrap - set this attribute to false to turn off word wrapping.

    TextArea supports TextChangeListener defined in its parent TextInputField interface. Text change events are processed asynchronously after typing in order not to block the typing.

    textArea.addTextChangeListener(event -> {
        int length = event.getText().length();
        textAreaLabel.setValue(length + " of " + textArea.getMaxLength());
    });
gui TextArea 2
  • textChangeEventMode defines the way the changes are transmitted to the server to cause a server-side event. There are 3 predefined event modes:

    • LAZY (default) - an event is triggered when there is a pause in editing the text. The length of the pause can be modified with setTextChangeTimeout() or using the textChangeTimeout attribute. A text change event is forced before a possible ValueChangeEvent, even if the user did not keep a pause while entering the text.

    • TIMEOUT - an event is triggered after a timeout period. If more changes are made during this period, the event sent to the server-side includes the changes made up to the last change. The length of the timeout can be set with setTextChangeTimeout() or using the textChangeTimeout attribute.

      If a ValueChangeEvent would occur before the timeout period, a TextChangeEvent is triggered before it, on the condition that the text content has changed since the previous TextChangeEvent.

    • EAGER - an event is triggered immediately for every change in the text content, typically caused by a key press. The requests are separate and are processed sequentially one after another. Change events are nevertheless communicated asynchronously to the server, so further input can be typed while event requests are being processed.

  • textChangeTimeout defines the time of the pause in editing the text or a timeout period, when the textChangeEventMode is LAZY or TIMEOUT.

    TextArea styles

    In Web Client with a Halo-based theme, you can set predefined styles to the TextArea component using the stylename attribute either in the XML descriptor or in the screen controller:

    <textArea id="textArea"
              stylename="borderless"/>

    When setting a style programmatically, select one of the HaloTheme class constants with the TEXTAREA_ prefix:

    textArea.setStyleName(HaloTheme.TEXTAREA_BORDERLESS);
    • align-center - align the text inside the area to center.

    • align-right - align the text inside the area to the right.

    • borderless - removes the border and background from the text area.



3.5.2.1.46. TextField

TextField is a component for text editing. It can be used both for working with entity attributes and entering/displaying arbitrary textual information.

XML-name of the component: textField

  • An example of a text field with a caption retrieved from the localized messages pack:

    <textField id="nameField" caption="msg://name"/>

    The figure below shows an example of a simple text field.

    gui textField data
  • In Web Client with a Halo-based theme, you can set predefined styles to the TextField component using the stylename attribute either in the XML descriptor or in the screen controller:

    <textField id="textField"
               stylename="borderless"/>

    When setting a style programmatically, select one of the HaloTheme class constants with the TEXTFIELD_ prefix:

    textField.setStyleName(HaloTheme.TEXTFIELD_INLINE_ICON);

    TextField styles:

    • align-center - align the text inside the field to center.

    • align-right - align the text inside the field to the right.

    • borderless - removes the border and background from the text field.

    • inline-icon - moves the default caption icon inside the text field.

    TextField supports automatic case conversion. The caseConvertion attribute can have one of the following values:

    • UPPER - the upper case,

    • LOWER - the lower case,

    • NONE - without conversion (the default value). Use this option to support key-sequence input using IME (for example, for Japanese, Korean or Chinese language).

  • To create a text field connected to data, use dataContainer and property attributes.

    <data>
        <instance id="customerDc" class="com.company.sales.entity.Customer" view="_local">
            <loader/>
        </instance>
    </data>
    <layout>
        <textField dataContainer="customerDc" property="name" caption="msg://name"/>
    </layout>

    As you can see in the example, the screen describes the customerDc data container for Customer entity, which has name attribute. The text field component has a link to the container specified in the dataContainer attribute; property attribute contains the name of the entity attribute that is displayed in the text field.

  • If the field is not connected to an entity attribute (i.e., the data container and attribute name are not set), you can set the data type using the datatype attribute. It is used to format field values. The attribute value accepts any data type registered in the application metadata – see Datatype. Typically, TextField uses the following data types:

    • decimal

    • double

    • int

    • long

    If the datatype attribute is specified for the field and the user enters an incorrect value, the default conversion error message appears.

    As an example, let’s look at a text field with an Integer data type.

    <textField id="integerField" datatype="int" caption="msg://integerFieldName"/>

    If a user enters a value that cannot be interpreted as an integer number, then when the field loses focus, the application will show a default error message.

    gui datatype default message

    Default error messages are specified in the main localized message pack and have the following template: databinding.conversion.error.<type>, e.g.:

    databinding.conversion.error.int = Must be Integer
  • You can specify a custom conversion error message declaratively in the screen XML descriptor using the conversionErrorMessage attribute:

    <textField conversionErrorMessage="This field can work only with Integers" datatype="int"/>

    or programmatically in the screen controller:

    textField.setConversionErrorMessage("This field can work only with Integers");
  • Text field can be assigned a validator – a class implementing Field.Validator interface. The validator limits user input in addition to what is already done by the datatype. For example, to create an input field for positive integer numbers, you need to create a validator class:

    public class PositiveIntegerValidator implements Field.Validator {
        @Override
        public void validate(Object value) throws ValidationException {
            Integer i = (Integer) value;
            if (i <= 0)
                throw new ValidationException("Value must be positive");
        }
    }

    and assign it as a validator to the text field with int datatype:

    <textField id="integerField" datatype="int">
        <validator class="com.sample.sales.gui.PositiveIntegerValidator"/>
    </textField>

    Unlike input check against the data type, validation is performed not when the field looses focus, but after invocation of the field’s validate() method. It means that the field (and the linked entity attribute) may temporarily contain a value that does not satisfy validation conditions (a non-positive number in the example above). This should not be an issue, because validated fields are typically used in editor screens, which automatically invoke validation for all their fields before commit. If the field is located not in an editing screen, the field’s validate() method should be invoked explicitly in the controller.

  • TextField supports TextChangeListener defined in its parent TextInputField interface. Text change events are processed asynchronously after typing in order not to block the typing.

    textField.addTextChangeListener(event -> {
        int length = event.getText().length();
        textFieldLabel.setValue(length + " of " + textField.getMaxLength());
    });
    textField.setTextChangeEventMode(TextInputField.TextChangeEventMode.LAZY);
    gui textfield 2
  • The TextChangeEventMode defines the way the changes are transmitted to the server to cause a server-side event. There are 3 predefined event modes:

    • LAZY (default) - an event is triggered when there is a pause in editing the text. The length of the pause can be modified with setInputEventTimeout(). A text change event is forced before a possible ValueChangeEvent, even if the user did not keep a pause while entering the text.

    • TIMEOUT - an event is triggered after a timeout period. If more changes are made during this period, the event sent to the server-side includes the changes made up to the last change. The length of the timeout can be set with setInputEventTimeout().

      If a ValueChangeEvent would occur before the timeout period, a TextChangeEvent is triggered before it, on the condition that the text content has changed since the previous TextChangeEvent.

    • EAGER - an event is triggered immediately for every change in the text content, typically caused by a key press. The requests are separate and are processed sequentially one after another. Change events are nevertheless communicated asynchronously to the server, so further input can be typed while event requests are being processed.

  • EnterPressListener allows you to define an action executed when Enter is pressed:

    textField.addEnterPressListener(enterPressEvent ->
            notifications.create()
                    .withCaption("Enter pressed")
                    .show());
  • ValueChangeListener is triggered on the value changes when the user finished the text input, i.e. after the Enter press or when the component loses focus. An event object of the ValueChangeEvent type is passed to the listener. It has the following methods:

    • getPrevValue() returns the value of the component before the change.

    • getValue() method return the current value of the component.

      textField.addValueChangeListener(stringValueChangeEvent ->
              notifications.create()
                      .withCaption("Before: " + stringValueChangeEvent.getPrevValue() +
                              ". After: " + stringValueChangeEvent.getValue())
                      .show());

      The origin of the ValueChangeEvent can be tracked using isUserOriginated() method.

  • If a text field is linked to an entity attribute (via datasource and property), and if the entity attribute has a length parameter defined in the @Column JPA-annotation, then the TextField will limit the maximum length of entered text accordingly.

    If a text field is not linked to an attribute, or if the attribute does not have length value defined, or this value needs to be overridden, then the maximum length of the entered text can be limited using maxLength attribute. The value of "-1" means there are no limitations. For example:

    <textField id="shortTextField" maxLength="10"/>
  • By default, text field trims spaces at the beginning and at the end of the entered string. I.e. if user enters " aaa bbb ", the value of the field returned by the getValue() method and saved to the linked entity attribute will be "aaa bbb". You can disable trimming of spaces by setting the trim attribute to false.

    It should be noted that trimming only works when users enter a new value. If the value of the linked attribute already has spaces in it, the spaces will be displayed until user edits the value.

  • Text field always returns null instead of an entered empty string. Therefore, with the trim attribute enabled, any string containing spaces only will be converted to null.

  • The setCursorPosition() method can be used to focus the field and set the cursor position to the specified 0-based index.



3.5.2.1.47. TimeField

TimeField is a field for displaying and entering time.

gui timeField

XML-name of the component: timeField.

  • To create a time field associated with data, dataContainer and property attributes should be used:

    <data>
        <instance id="orderDc" class="com.company.sales.entity.Order" view="_local">
            <loader/>
        </instance>
    </data>
    <layout>
        <timeField dataContainer="orderDc" property="deliveryTime"/>
    </layout>

    As you can see in the example above, the screen defines the orderDc data container for Order entity, which has deliveryTime attribute. The dataContainer attribute of the time input component contains a link to the container, and the property attribute – the name of the entity attribute displayed in the field.

    Related entity attribute should have java.util.Date or java.sql.Time type.

  • If the field is not connected to an entity attribute (i.e. the data container and attribute name are not set), you can set the data type using the datatype attribute. TimeField uses the following data types:

    • localTime

    • offsetTime

    • time

  • The time format is defined by the time datatype and is specified in the main localized messages pack in the timeFormat key.

  • The time format can also be specified in the timeFormat attribute. It can be either a format string, or a key in a message pack (with the msg:// prefix).

  • Regardless of the mentioned above format display of seconds can be controlled using showSeconds attribute. By default, seconds are displayed if the format contains ss.

    <timeField dataContainer="orderDc" property="createTs" showSeconds="true"/>
    gui timeFieldSec


3.5.2.1.48. TokenList

TokenList component offers a simplified way of working with lists: instance names are listed vertically or horizontally, adding is done using drop-down list, removal – using the buttons located near each instance.

gui tokenList

XML-name of the component: tokenList

Below is an example description of TokenList in an XML-descriptor of a screen:

<data>
    <instance id="orderDc" class="com.company.sales.entity.Order" view="order-edit">
        <loader/>
        <collection id="productsDc" property="products"/>
    </instance>
    <collection id="allProductsDc" class="com.company.sales.entity.Product" view="_minimal">
        <loader id="allProductsDl">
            <query><![CDATA[select e from sales_Product e order by e.name]]></query>
        </loader>
    </collection>
</data>
<layout>
    <tokenList id="productsList" dataContainer="orderDc" property="products" inline="true" width="500px">
        <lookup optionsContainer="allProductsDc"/>
    </tokenList>
</layout>

In the example above, the nested productsDc data container which includes a collection of products within an order is defined, as well as allProductsDs data container containing a collection of all products available in the database. The TokenList component with productsList identifier displays the content of the productsDc container and enables changing the collection by adding instances from allProductsDc.

tokenList attributes:

  • position – sets the position for the drop-down list. The attribute can take two values: TOP, BOTTOM. Default is TOP.

gui tokenListBottom
  • inline attribute defines how the list with selected items will be displayed: vertically or horizontally. true corresponds to horizontal alignment, false – to vertical. An example of a component with horizontal alignment:

gui tokenListInline
  • simple – when set to true, the items selection component will be hidden with only the Add button left. Clicking the Add button opens the screen with the list of entity instances which type is defined by the data container. Selection screen identifier is selected according to the rules for the PickerField standard lookup action. Clicking the Clear button will remove all elements from the TokenList component’s container.

    gui tokenListSimple withClear
  • clearEnabled - when set to false, the Clear button is hidden.

tokenList elements:

  • lookup − values selection component descriptor.

    Attributes of the lookup element:

    • lookup attribute makes it possible to select items using an entity lookup screen:

      gui tokenListLookup
    • inputPrompt - a textual prompt that is displayed in the lookup field to prompt the user for input. If not set, the lookup is empty.

      <tokenList id="linesList" dataContainer="orderItemsDс" property="items" width="320px">
          <lookup optionsDatasource="allItemsDs" inputPrompt="Choose an item"/>
      </tokenList>
      gui TokenList inputPrompt
    • lookupScreen attribute sets the identifier of the screen used for items selection in lookup="true" mode. If this attribute is not set, screen identifier is selected according to the rules for the com.haulmont.cuba.gui.actions.picker.LookupAction standard action.

    • openType attribute defines how the lookup screen will be opened, similar to what is described for the com.haulmont.cuba.gui.actions.picker.LookupAction standard action. Default value – THIS_TAB.

    • multiselect - if this attribute’s value is set to true, then true will be passed to parameters map of the lookup screen for the MULTI_SELECT key. This flag can be used to set the screen into multiple selection mode. This flag is defined in the WindowParams enum, and in legacy screens it is convenient to work with it in the following way:

      @Override
      public void init(Map<String, Object> params) {
          if (WindowParams.MULTI_SELECT.getBool(getContext())) {
              usersTable.setMultiSelect(true);
          }
      }
  • addButton – descriptor of the button for adding items. Can contain caption and icon attributes.

tokenList listeners:

  • ItemClickListener allows you to track the click on tokenList items.

  • ValueChangeListener enables tracking the changes of the tokenList value, as well as of any other components implementing the Field interface. The origin of the ValueChangeEvent can be tracked using isUserOriginated() method.



3.5.2.1.49. Tree

The Tree component is intended to display hierarchical structures represented by entities referencing themselves.

gui Tree

XML-name of the component: tree

Below is an example of the Tree component description in a screen XML-descriptor:

<data readOnly="true">
    <collection id="departmentsDc" class="com.company.sales.entity.Department" view="department-view">
        <loader id="departmentsDl">
            <query>
                <![CDATA[select e from sales_Department e]]>
            </query>
        </loader>
    </collection>
</data>
<layout>
    <tree id="departmentsTree" dataContainer="departmentsDc" hierarchyProperty="parentDept"/>
</layout>

Here, the dataContainer attribute contains a reference to a collection container, and the hierarchyProperty attribute defines the name of the entity attribute which is a reference to same entity.

The name of the entity attribute to be displayed in the tree can be set using the captionProperty attribute. If this attribute is not defined, the screen will show the entity instance name.

The setItemCaptionProvider() method sets a function for placing the name of the entity attribute as a caption for each item of the tree.

Selection in Tree:

  • multiselect attribute enables setting multiple selection mode for tree items. If multiselect is true, users can select multiple items using keyboard or mouse holding Ctrl or Shift keys. By default, multiple selection mode is switched off.

  • selectionMode - sets the rows selection mode. There are 3 predefined selection modes:

    • SINGLE - single record selection.

    • MULTI - multiple selection as in any table.

    • NONE - selection is disabled.

    Rows selection events can be tracked by SelectionListener. The origin of the selection event can be tracked using isUserOriginated() method.

    The selectionMode attribute has priority over the deprecated multiselect attribute.

The setItemClickAction() method may be used to define an action that will be performed when a tree node is double-clicked.

Each tree item can have an icon on the left. Create an implementation of the Function interface in the setIconProvider() method in the screen controller:

@Inject
private Tree<Department> tree;

@Subscribe
protected void onInit(InitEvent event) {
    tree.setIconProvider(department -> {
        if (department.getParentDept() == null) {
            return "icons/root.png";
        }
        return "icons/leaf.png";
    });
}

For old screens, the Tree component can be bound to a datasource instead of data container. In this case, the nested treechildren element should be defined and contain a reference to a hierarchicalDatasource in the datasource attribute. Declaration of a hierarchicalDatasource should contain a hierarchyProperty attribute – the name of the entity attribute which is a reference to same entity.



3.5.2.1.50. TreeDataGrid

TreeDataGrid, similarly to the DataGrid component, is designed to display and sort tabular data, and provides means to display hierarchical data and manipulate rows and columns with greater performance due to lazy loading of data while scrolling.

The component is used for entities that have references to themselves. For example, it can be a file system or a company organization chart.

gui TreeDataGrid

XML name of the component: treeDataGrid.

For TreeDataGrid, two attributes should be set: dataContainer which binds treeDataGrid to a data container, and hierarchyProperty which is the name of the entity attribute which references the same entity.

An example of component definition in an XML-descriptor of a screen:

<data readOnly="true">
    <collection id="departmentsDc" class="com.company.sales.entity.Department" view="department-view">
        <loader id="departmentsDl">
            <query>
                <![CDATA[select e from sales_Department e]]>
            </query>
        </loader>
    </collection>
</data>
<layout>
    <treeDataGrid id="treeDataGrid" dataContainer="departmentsDc" hierarchyProperty="parentDept">
        <columns>
            <column id="name" property="name"/>
            <column id="parentDept" property="parentDept"/>
        </columns>
    </treeDataGrid>
</layout>

The functionality of TreeDataGrid is similar to a simple DataGrid.


3.5.2.1.51. TreeTable

TreeTable component is a hierarchical table displaying a tree-like structure in the leftmost column. The component is used for entities that have references to themselves. For example, it can be a file system or a company organization chart.

gui treeTable

XML-name of the component: treeTable

The dataContainer attribute of the treeTable component should contain a reference to a collection container, and the hierarchyProperty attribute defines the name of the entity attribute which is a reference to same entity.

Below is an example of component description in a screen XML descriptor:

<data readOnly="true">
    <collection id="departmentsDc" class="com.company.sales.entity.Department" view="_local">
        <loader id="departmentsDl">
            <query>
                <![CDATA[select e from sales_Department e]]>
            </query>
        </loader>
    </collection>
</data>
<layout>
    <treeTable id="departmentsTable" dataContainer="departmentsDc" hierarchyProperty="parentDept" width="100%">
        <columns>
            <column id="name"/>
            <column id="active"/>
        </columns>
    </treeTable>
</layout>

The functionality of TreeTable is similar to a simple Table.



3.5.2.1.52. TwinColumn

TwinColumn is a twin list component for multiple items selection. The left part of the list contains available unselected values, the right part – selected values. Users select the values by transferring them from the left to the right and backward using double click or dedicated buttons. A unique representation style and an icon can be defined for each value.

TwinColumn

XML name of the component: twinColumn

Below is an example of a twinColumn component usage to select entity instances:

<data>
    <instance id="orderDc" class="com.company.sales.entity.Order" view="order-edit">
        <loader/>
        <collection id="productsDc" property="products"/>
    </instance>
    <collection id="allProductsDc" class="com.company.sales.entity.Product" view="_minimal">
        <loader>
            <query>
                <![CDATA[select e from sales_Product e]]>
            </query>
        </loader>
    </collection>
</data>
<layout>
    <twinColumn id="twinColumn"
                dataContainer="productsDc"
                property="name"
                optionsContainer="allProductsDc"/>
</layout>

In this example, the twinColumn component will display names of Product entity instances located in the allProductsDc data container, and its getValue() method will return a collection of selected instances.

addAllBtnEnabled attribute shows the buttons moving all items between the lists.

columns attribute is used to set the number of characters in a row, and the rows attribute – to set the number of rows in each list.

leftColumnCaption and rightColumnCaption attributes can be used to set captions for the columns.

The presentation of the items can be defined by implementing the TwinColumn.StyleProvider interface and returning a style name and icon path for each entity instance displayed in the component.

The list of component options can be specified arbitrarily using the setOptionsList(), setOptionsMap() and setOptionsEnum() methods as described for the CheckBoxGroup component.



3.5.2.2. Containers
3.5.2.2.1. Accordion

Accordion is a container for collapsible content that allows you to toggle between hiding and showing large amount of content. The Accordion container is implemented for Web Client.

gui accordion

XML-name of the component: accordion. An example description of an accordion in a screen XML-descriptor:

<accordion id="accordion" height="100%">
    <tab id="tabStamford" caption="msg://tabStamford" margin="true" spacing="true">
        <label value="msg://sampleStamford"/>
    </tab>
    <tab id="tabBoston" caption="msg://tabBoston" margin="true" spacing="true">
        <label value="msg://sampleBoston"/>
    </tab>
    <tab id="tabLondon" caption="msg://tabLondon" margin="true" spacing="true">
        <label value="msg://sampleLondon"/>
    </tab>
</accordion>

The accordion component should contain nested tab elements describing tabs. Each tab is a container with a vertical components layout similar to vbox. Accordion container can be used if the application page is limited in space or the tab title is too long to be displayed in the TabSheet. Accordion is featured with a smooth transition animation.

tab element attributes:

  • id – tab identifier. Please note that tabs are not components and their IDs are used only within the Accordion in order to work with tabs from the controller.

  • caption – tab caption.

  • icon - defines icon location in theme catalog or the icon name in the icon set. Detailed information on recommended icon placement is available in Icons.

  • lazy – sets lazy loading for tab content.

    Lazy tabs do not load their content when the screen is opened, which reduces the number of components in memory. Components within a tab are loaded only when a user selects the tab. Additionally, if a lazy-tab includes visual components linked to a data container with a loader, the loader will not be triggered. As a result, screen opens faster, and data is loaded only when the user requests it by selecting this tab.

    Please note that the components located on a lazy tab do not exist when the screen is opened. Therefore they cannot be injected into a controller and cannot be obtained by invoking getComponent() in the controller’s init() method. The lazy tab components can be accessed only after the user opens the tab. This moment may be intercepted using Accordion.SelectedTabChangeListener, for example:

    @Inject
    private Accordion accordion;
    
    private boolean tabInitialized;
    
    @Subscribe
    protected void onInit(InitEvent event) {
        accordion.addSelectedTabChangeListener(selectedTabChangeEvent -> {
            if ("tabCambridge".equals(selectedTabChangeEvent.getSelectedTab().getName())) {
                initCambridgeTab();
            }
        });
    }
    
    private void initCambridgeTab() {
        if (tabInitialized) {
            return;
        }
        tabInitialized = true;
        (1)
    }
    1 Write the initialization code here. Use getComponentNN("comp_id") here to get lazy tab’s components.

    By default, tabs are not lazy, which means that all their content is loaded when a screen is opened.

  • In Web Client with a Halo-based theme, the stylename attribute allows you to set the predefined borderless style to the accordion component to remove borders and background:

    accordion.setStyleName(HaloTheme.ACCORDION_BORDERLESS);

An accordion tab can contain any other visual container, such as table, grid etc:

<accordion id="accordion" height="100%" width="100%" enable="true">
    <tab id="tabNY" caption="msg://tabNY" margin="true" spacing="true">
        <table id="nYTable" width="100%">
            <columns>
                <column id="borough"/>
                <column id="county"/>
                <column id="population"/>
                <column id="square"/>
            </columns>
            <rows datasource="newYorkDs"/>
        </table>
    </tab>
</accordion>
gui accordion 2


3.5.2.2.2. BoxLayout

BoxLayout is a container with sequential placement of components.

There are three types of BoxLayout, identified by the XML-elements:

  • hbox − components are placed horizontally.

    gui hbox
    <hbox spacing="true" margin="true">
        <dateField dataContainer="orderDc" property="date"/>
        <lookupField dataContainer="orderDc" property="customer" optionsContainer="customersDc"/>
        <textField dataContainer="orderDc" property="amount"/>
    </hbox>
  • vbox − components are placed vertically. vbox has 100% width by default.

    gui vbox
    <vbox spacing="true" margin="true">
        <dateField dataContainer="orderDc" property="date"/>
        <lookupField dataContainer="orderDc" property="customer" optionsContainer="customersDc"/>
        <textField dataContainer="orderDc" property="amount"/>
    </vbox>
  • flowBox − components are placed horizontally with line wrapping. If there is not enough space in a line, the components that do not fit will be displayed in the next line (the behavior is similar to Swing FlowLayout).

    gui flowbox
    <flowBox spacing="true" margin="true">
        <dateField dataContainer="orderDc" property="date"/>
        <lookupField dataContainer="orderDc" property="customer" optionsContainer="customersDc"/>
        <textField dataContainer="orderDc" property="amount"/>
    </flowBox>

In Web Client with a Halo-based theme, BoxLayout may be used to create enhanced composite layouts. The stylename attribute with card or well value along with stylename="v-panel-caption" for an enclosed container will make a component look like Vaadin Panel.

  • card style name makes a layout look like a card.

  • well style makes the card look "sunken" with shaded background.

gui boxlayout
<vbox stylename="well"
      height="200px"
      width="300px"
      expand="message"
      spacing="true">
    <hbox stylename="v-panel-caption"
          width="100%">
        <label value="Widget caption"/>
        <button align="MIDDLE_RIGHT"
                icon="font-icon:EXPAND"
                stylename="borderless-colored"/>
    </hbox>
    <textArea id="message"
              inputPrompt="Enter your message here..."
              width="280"
              align="MIDDLE_CENTER"/>
    <button caption="Send message"
            width="100%"/>
</vbox>

The getComponent() method allows you to obtain a child component of BoxLayout by its index:

Button button = (Button) hbox.getComponent(0);

You can use keyboard shortcuts in BoxLayout. Set the shortcut and the action to be performed using the addShortcutAction() method:

flowBox.addShortcutAction(new ShortcutAction("SHIFT-A", shortcutTriggeredEvent ->
        notifications.create()
                .withCaption("SHIFT-A action")
                .show()
));


3.5.2.2.3. ButtonsPanel

ButtonsPanel is a container that streamlines the use and placement of the components (usually, buttons) for data management in a table.

gui buttonsPanel

XML-name of the component: buttonsPanel.

A sample definition of ButtonsPanel in screen XML-descriptor:

<table id="customersTable" dataContainer="customersDc" width="100%">
    <actions>
        <action id="create" type="create"/>
        <action id="edit" type="edit"/>
        <action id="remove" type="remove"/>
        <action id="excel" type="excel"/>
    </actions>
    <columns>
        <column id="name"/>
        <column id="email"/>
    </columns>
    <rowsCount/>
    <buttonsPanel id="buttonsPanel" alwaysVisible="true">
        <button id="createBtn" action="customersTable.create"/>
        <button id="editBtn" action="customersTable.edit"/>
        <button id="removeBtn" action="customersTable.remove"/>
        <button id="excelBtn" action="customersTable.excel"/>
    </buttonsPanel>
</table>

buttonsPanel element can be located either inside a table, or in any other place of a screen.

If the buttonsPanel is located in a table, it is combined with the table’s rowsCount component thus using vertical space more effectively. Additionally, if a lookup screen is opened using Frame.openLookup() (for example, from the PickerField component) the buttons panel becomes hidden.

alwaysVisible attribute disables panel hiding in a lookup screen when it is opened by Frame.openLookup(). If the attribute value is true, the buttons panel is not hidden. By default, the attribute value is false.

Clicks on the buttonsPanel area can be intercepted with the help of the LayoutClickListener interface.

You can use keyboard shortcuts in ButtonsPanel. Set the shortcut and the action to be performed using the addShortcutAction() method:

buttonsPanel.addShortcutAction(new ShortcutAction("SHIFT-A", shortcutTriggeredEvent ->
        notifications.create()
                .withCaption("SHIFT-A action")
                .show()
));


3.5.2.2.4. CssLayout

CssLayout is a container that enables full control over placement and styling of enclosed components using CSS.

XML-name of the component: cssLayout.

Below is an example of using cssLayout for simple responsive screen.

Displaying components on a wide screen:

gui cssLayout 1

Displaying components on a narrow screen:

gui cssLayout 2

Screen’s XML-descriptor:

<cssLayout responsive="true" stylename="responsive-container" width="100%">
    <vbox margin="true" spacing="true" stylename="group-panel">
        <textField caption="Field One" width="100%"/>
        <textField caption="Field Two" width="100%"/>
        <button caption="Button"/>
    </vbox>
    <vbox margin="true" spacing="true" stylename="group-panel">
        <textField caption="Field Three" width="100%"/>
        <textField caption="Field Four" width="100%"/>
        <button caption="Button"/>
    </vbox>
</cssLayout>

Content of modules/web/themes/halo/halo-ext.scss file (see Extending an Existing Theme for how to create this file):

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

  .responsive-container {
    &[width-range~="0-900px"] {
      .group-panel {
        width: 100% !important;
      }
    }

    &[width-range~="901px-"] {
      .group-panel {
        width: 50% !important;
      }
    }
  }
}
  • stylename attribute enables setting predefined styles to the CssLayout component either in the XML descriptor or in the screen controller.

    • v-component-group style is used to create a grouped set of components, i.e. a row of components which are joined seamlessly together:

      <cssLayout stylename="v-component-group">
          <textField inputPrompt="Search..."/>
          <button caption="OK"/>
      </cssLayout>
      gui cssLayout 3
    • well style makes container look "sunken" with shaded background.

    • card style name makes a layout look like a card. Combined with an additional v-panel-caption style name for any enclosed layout, it provides a possibility to create enhanced composite layouts, for example:

      <cssLayout height="300px"
                 stylename="card"
                 width="300px">
          <hbox stylename="v-panel-caption"
                width="100%">
              <label value="Widget caption"/>
              <button align="MIDDLE_RIGHT"
                      icon="font-icon:EXPAND"
                      stylename="borderless-colored"/>
          </hbox>
          <vbox height="100%">
              <label value="Panel content"/>
          </vbox>
      </cssLayout>

      and result will be the following:

      gui cssLayout 4


3.5.2.2.5. Frame

frame element is designed to include frames into a screen.

Attributes:

  • src − path to the frame XML-descriptor.

  • screen – frame identifier in screens.xml (if the frame is registered).

One of these attributes should be defined. If both attributes are defined, frame will be loaded from the file explicitly set in src.



3.5.2.2.6. GridLayout

GridLayout container places components on a grid.

gui gridlayout

XML-name of the component: grid.

Example container usage:

<grid spacing="true">
    <columns count="4"/>
    <rows>
        <row>
            <label value="Date" align="MIDDLE_LEFT"/>
            <dateField dataContainer="orderDc" property="date"/>
            <label value="Customer" align="MIDDLE_LEFT"/>
            <lookupField dataContainer="orderDc" property="customer" optionsContainer="customersDc"/>
        </row>
        <row>
            <label value="Amount" align="MIDDLE_LEFT"/>
            <textField dataContainer="orderDc" property="amount"/>
        </row>
    </rows>
</grid>

grid elements:

  • columns – a required element, describes grid columns. It should have either a count attribute, or a nested column element for each column.

    In the simplest case, it is enough to set the number of columns in the count attribute. Then, if the container width is explicitly defined in pixels or percents, free space will be divided between the columns equally.

    In order to divide screen space non-equally, a column element with a flex attribute should be defined for each column.

    An example of a grid where the second and the fourth columns take all extra horizontal space and the fourth column takes three times more space:

    <grid spacing="true" width="100%">
        <columns>
            <column/>
            <column flex="1"/>
            <column/>
            <column flex="3"/>
        </columns>
        <rows>
            <row>
                <label value="Date"/>
                <dateField dataContainer="orderDc" property="date" width="100%"/>
                <label value="Customer"/>
                <lookupField dataContainer="orderDc" property="customer" optionsContainer="customersDc" width="100%"/>
            </row>
            <row>
                <label value="Amount"/>
                <textField dataContainer="orderDc" property="amount" width="100%"/>
            </row>
        </rows>
    </grid>

    If flex is not defined, or is set to 0, the width of the column will be set according to its contents given that at least one other column has a non-zero flex. In the example above, the first and the third columns will get the width according to the maximum text length.

    In order for the free space to appear, the entire container width should be set in either pixels or percents. Otherwise, column width will be calculated according to content length, and flex attribute will have no effect.

  • rows − a required element, contains a set of rows. Each line is defined in its own row element.

    row element can have a flex attribute similar to the one defined for column, but affecting the distribution of free vertical space with a given total grid height.

    row element should contain elements of the components displayed in the grid’s current row cells. The number of components in a row should not exceed the defined number of columns, but it can be less.

Any component located in a grid container can have colspan and rowspan attributes. These attributes set the number of columns and rows occupied by the corresponding component. For example, this is how Field3 field can be extended to cover three columns:

<grid spacing="true">
    <columns count="4"/>
    <rows>
        <row>
            <label value="Name 1"/>
            <textField/>
            <label value="Name 2"/>
            <textField/>
        </row>
        <row>
            <label value="Name 3"/>
            <textField colspan="3" width="100%"/>
        </row>
    </rows>
</grid>

As a result, the components will be placed in the following way:

gui gridlayout colspan

Clicks on the GridLayout area can be intercepted with the help of the LayoutClickListener interface.

The getComponent() method allows you to obtain a child component of GridLayout by its column and row index:

Button button = (Button) gridLayout.getComponent(0,1);

You can use keyboard shortcuts in GridLayout. Set the shortcut and the action to be performed using the addShortcutAction() method:

grid.addShortcutAction(new ShortcutAction("SHIFT-A", shortcutTriggeredEvent ->
        notifications.create()
                .withCaption("SHIFT-A action")
                .show()
));


3.5.2.2.7. GroupBoxLayout

GroupBoxLayout is a container that enables framing the embedded components and setting a universal header for them. Additionally, it can collapse content.

gui groupBox

Component XML-name: groupBox.

An example container description in a screen XML-descriptor:

<groupBox caption="Order">
    <dateField dataContainer="orderDc" property="date" caption="Date"/>
    <lookupField dataContainer="orderDc" property="customer" optionsContainer="customersDc" caption="Customer"/>
    <textField dataContainer="orderDc" property="amount" caption="Amount"/>
</groupBox>

groupBox attributes:

  • caption – group header.

  • orientation – defines components placement direction − horizontal or vertical. The default value is vertical.

  • collapsable – if the value is set to true, the component’s content can be hidden using gui_groupBox_minus/gui_groupBox_plus buttons.

  • collapsed – if set to true, component’s content will be collapsed initially. It is used with collapsable="true".

    An example of a collapsed GroupBox:

    gui groupBox collapsed

    The expanded state change events of the groupBox component can be intercepted with the help of the ExpandedStateChangeListener interface.

  • outerMargin - sets the outer margins outside the border of groupBox. If set to true, the outer margins will be added on all sides of the component. To set the outer margins for all sides individually, set true or false for each side of groupBox:

    <groupBox outerMargin="true, false, true, false">

    If the showAsPanel attribute is set to true, outerMargin is ignored.

  • showAsPanel – if set to true, the component will look like Vaadin Panel. The default value is false.

    gui groupBox Panel

By default, the groupBox container is 100% wide, similar to vbox.

In Web Client with a Halo-based theme, you can set predefined styles to the groupBox component using the stylename attribute either in the XML descriptor or in the screen controller. When setting a style programmatically, select one of the HaloTheme class constants with the LAYOUT_ or GROUPBOX_ prefix. The following styles should be used combined with showAsPanel attribute set to true:

  • borderless style removes borders and the background color of the groupBox:

    groupBox.setShowAsPanel(true);
    groupBox.setStyleName(HaloTheme.GROUPBOX_PANEL_BORDERLESS);
  • card style name makes a layout look like a card.

  • well style makes container look "sunken" with shaded background:

    <groupBox caption="Well-styled groupBox"
              showAsPanel="true"
              stylename="well"
              width="300px"
              height="200px"/>
    gui groupBox Panel 2

You can use keyboard shortcuts in Groupbox. Set the shortcut and the action to be performed using the addShortcutAction() method:

groupBox.addShortcutAction(new ShortcutAction("SHIFT-A", shortcutTriggeredEvent ->
        notifications.create()
                .withCaption("SHIFT-A action")
                .show()
));


3.5.2.2.8. HtmlBoxLayout

HtmlBoxLayout is a container that enables you to define locations of components in an HTML template. The layout template is included in a theme.

Do not use HtmlBoxLayout for dynamic content or if you want to embed JavaScript code. Use BrowserFrame instead.

XML-name of the component: htmlBox.

Below is an example of using htmlBox for a simple screen.

gui htmlBox 1

Screen’s XML-descriptor:

<htmlBox align="TOP_CENTER"
         template="sample"
         width="500px">
    <label id="logo"
           value="Subscribe"
           stylename="logo"/>
    <textField id="email"
               width="100%"
               inputPrompt="email@test.test"/>
    <button id="submit"
            width="100%"
            invoke="showMessage"
            caption="Subscribe"/>
</htmlBox>

htmlBox attributes:

  • template attribute defines the name of an HTML file located in the layouts subdirectory of your theme. You should create a theme extension or a custom theme before creating a template.

    For example, if your theme is Halo and the attribute contains my_template, the template file should be modules/web/themes/halo/layouts/my_template.html.

    Content of the HTML template located in the modules/web/themes/halo/layouts/sample.html file:

    <div location="logo" class="logo"></div>
    <table class="component-container">
        <tr>
            <td>
                <div location="email" class="email"></div>
            </td>
            <td>
                <div location="submit" class="submit"></div>
            </td>
        </tr>
    </table>

    A template should contain <div> elements with location attributes. This elements will display CUBA components defined in the XML descriptor with corresponding identifiers.

    Content of modules/web/themes/halo/com.company.application/halo-ext.scss file (see Extending an Existing Theme for how to create this file):

    @mixin com_company_application-halo-ext {
      .email {
        width: 390px;
      }
    
      .submit {
        width: 100px;
      }
    
      .logo {
        font-size: 96px;
        text-transform: uppercase;
        margin-top: 50px;
      }
    
      .component-container {
        display: inline-block;
        vertical-align: top;
        width: 100%;
      }
    }
  • templateContents attribute sets the contents of the template and is used to draw the custom layout directly.

    For example:

    <htmlBox height="256px"
             width="400px">
        <templateContents>
            <![CDATA[
                <table align="center" cellspacing="10"
                       style="width: 100%; height: 100%; color: #fff; padding: 20px;    background: #31629E repeat-x">
                    <tr>
                        <td colspan="2"><h1 style="margin-top: 0;">Login</h1>
                        <td>
                    </tr>
                    <tr>
                        <td align="right">User&nbsp;name:</td>
                        <td>
                            <div location="username"></div>
                        </td>
                    </tr>
                    <tr>
                        <td align="right">Password:</td>
                        <td>
                            <div location="password"></div>
                        </td>
                    </tr>
                    <tr>
                        <td align="right" colspan="2">
                            <div location="okbutton" style="padding: 10px;"></div>
                        </td>
                    </tr>
                    <tr>
                        <td colspan="2" style="padding: 7px; background-color: #4172AE"><span
                                style="font-family: FontAwesome; margin-right: 5px;">&#xf05a;</span> This information is in the layout.
                        <td>
                    </tr>
                </table>
            ]]>
        </templateContents>
        <textField id="username"
                   width="100%"/>
        <textField id="password"
                   width="100%"/>
        <button id="okbutton"
                caption="Login"/>
    </htmlBox>


3.5.2.2.9. Layout

layout is the root element of the screen layout. It is a container with a vertical layout of components, similar to vbox.

Attributes of layout:

  • spacing - sets spacing between components within the layout.

  • margin - defines indentation between the outer borders and the layout content.

  • expand - defines a component within the layout that should be expanded to use all available space in the direction of component placement.

  • responsive - indicates that the container should react on change in the available space.

  • stylename - defines a style name for the layout.

  • height - sets the layout’s height.

  • width - sets the layout’s width.

  • maxHeight - sets maximum CSS height for window layout. For example, "640px", "100%".

  • minHeight - sets minimum CSS height for window layout. For example, "640px", "auto".

  • maxWidth - sets maximum CSS width for window layout. For example, "640px", "100%".

  • minWidth - sets minimum CSS width for window layout. For example, "640px", "auto".

For example:

<layout minWidth="600px"
        minHeight="200px">
    <textArea width="800px"/>
</layout>
layout 1
Figure 20. Full-size textArea inside a layout
layout 2
Figure 21. Scroll bars appear when the window size is less than the layout’s minimal dimensions

These attributes work in the dialog mode as well:

<dialogMode forceDialog="true"
            width="500"
            height="250"/>
<layout minWidth="600px"
        minHeight="200px">
    <textArea width="250px"/>
</layout>
layout 3
Figure 22. Scroll bars appear when the dialog size is less than the layout’s minimal dimensions
3.5.2.2.10. ScrollBoxLayout

ScrollBoxLayout − a container that supports content scrolling.

gui scrollBox

Component XML-name: scrollBox

An example container description in a screen XML-descriptor:

<groupBox caption="Order" width="300" height="170">
    <scrollBox width="100%" height="100%" spacing="true" margin="true">
        <dateField dataContainer="orderDc" property="date" caption="Date"/>
        <lookupField dataContainer="orderDc" property="customer" optionsContainer="customersDc" caption="Customer"/>
        <textField dataContainer="orderDc" property="amount" caption="Amount"/>
    </scrollBox>
</groupBox>
  • The components placement direction can be defined by orientation attribute − horizontal or vertical. Default is vertical.

  • scrollBars attribute enables configuring scroll bars. It can be horizontal, vertical – for horizontal and vertical scrolling respectively, both – for scrolling in both directions. Setting the value to none forbids scrolling in any direction.

  • contentHeight - sets content height.

  • contentWidth - sets content width.

  • contentMaxHeight - sets maximum CSS height for content, for example, "640px", "100%".

  • contentMinHeight - sets minimum CSS height for content, for example, "640px", "auto".

  • contentMaxWidth - sets maximum CSS width for content, for example, "640px", "100%".

  • contentMinWidth - sets minimum CSS width for content, for example, "640px", "auto".

<layout>
    <scrollBox contentMinWidth="600px"
               contentMinHeight="200px"
               height="100%"
               width="100%">
        <textArea height="150px"
                  width="800px"/>
    </scrollBox>
</layout>
gui scrollBox 1
Figure 23. Full-size scrollBox with textArea inside
gui scrollBox 2
Figure 24. Scroll bar appears to maintain the content width when the window is resized

It is recommended to set the content width and height. Otherwise, the components placed in the scrollBox should have fixed size or default size.

Do not set the size of nested components to height="100%" or width="100%" if the content width and height are not set.

You can use keyboard shortcuts in ScrollBox. Set the shortcut and the action to be performed using the addShortcutAction() method:

scrollBox.addShortcutAction(new ShortcutAction("SHIFT-A", shortcutTriggeredEvent ->
        notifications.create()
                .withCaption("SHIFT-A action")
                .show()
));


3.5.2.2.11. SplitPanel

SplitPanel − a container divided into two areas by a movable separator.

gui splitPanel

Component XML-name: split.

An example description of a split panel in a screen XML-descriptor:

<split orientation="horizontal" pos="30" width="100%" height="100%">
    <vbox margin="true" spacing="true">
        <dateField dataContainer="orderDc" property="date" caption="Date"/>
        <lookupField dataContainer="orderDc" property="customer" optionsContainer="customersDc" caption="Customer"/>
    </vbox>
    <vbox margin="true" spacing="true">
        <textField dataContainer="orderDc" property="amount" caption="Amount"/>
    </vbox>
</split>

split container must contain two nested containers or components. They will be displayed on both sides of the separator.

split attributes:

  • dockable - enables or disables the SplitPanel dock button, the default value is false.

    gui SplitPanel dockable

    Docking is available only for horizontally-oriented SplitPanel.

  • dockMode - defines the docking direction. Possible values: LEFT or RIGHT.

    <split orientation="horizontal"
           dockable="true"
           dockMode="RIGHT">
        ...
    </split>
  • minSplitPosition, maxSplitPosition - defines a range of the available position of the split which can be set in pixels or percents.

    For example, you can restrict moving the splitter between 100 and 300 pixels from the left side of the component as follows:

    <split id="splitPanel" maxSplitPosition="300px" minSplitPosition="100px" width="100%" height="100%">
        <vbox margin="true" spacing="true">
            <button caption="Button 1"/>
            <button caption="Button 2"/>
        </vbox>
        <vbox margin="true" spacing="true">
            <button caption="Button 4"/>
            <button caption="Button 5"/>
        </vbox>
    </split>

    If you want to set the range programmatically, specify a unit of value with Component.UNITS_PIXELS or Component.UNITS_PERCENTAGE:

    splitPanel.setMinSplitPosition(100, Component.UNITS_PIXELS);
    splitPanel.setMaxSplitPosition(300, Component.UNITS_PIXELS);
  • orientation – defines component orientation. horizontal – nested components are placed horizontally, vertical – they are placed vertically.

  • pos – an integer number defining percentage of the first component area compared to the second one. For example, pos="30" means that the areas ration is 30/70. By default the areas are divided 50/50.

  • reversePosition - indicates that the pos attribute specifies a position of the splitter from the opposite side of the component.

  • If the locked attribute is set to true, users are unable to change the separator position.

  • The stylename attribute with the large value makes the split handle wider.

    split.setStyleName(HaloTheme.SPLITPANEL_LARGE);

SplitPanel methods:

  • You can get a position of the splitter using the getSplitPosition() method.

  • The events of moving the splitter can be intercepted with the help of PositionUpdateListener. The origin of the SplitPositionChangeEvent can be tracked using isUserOriginated() method.

  • If you need to get a unit of splitter position, use getSplitPositionUnit() method. It will return Component.UNITS_PIXELS or Component.UNITS_PERCENTAGE.

  • isSplitPositionReversed() returns true if position is set from the opposite side of the component.

The appearance of the SplitPanel component can be customized using SCSS variables with $cuba-splitpanel-* prefix. You can change these variables in the visual editor after creating a theme extension or a custom theme.



3.5.2.2.12. TabSheet

TabSheet container is a tabbed panel. The panel shows content of one tab at a time.

gui tabsheet

XML-name of the component: tabSheet.

An example description of a tabbed panel in a screen XML-descriptor:

<tabSheet>
    <tab id="mainTab" caption="Tab1" margin="true" spacing="true">
        <dateField dataContainer="orderDc" property="date" caption="Date"/>
        <lookupField dataContainer="orderDc" property="customer" optionsContainer="customersDc" caption="Customer"/>
    </tab>
    <tab id="additionalTab" caption="Tab2" margin="true" spacing="true">
        <textField dataContainer="orderDc" property="amount" caption="Amount"/>
    </tab>
</tabSheet>

The description attribute of tabSheet defines a hint which is displayed in a popup when a user hovers the mouse cursor over or clicks on the tabs area.

gui tabsheet description

The tabSheet component should contain nested tab elements describing tabs. Each tab is a container with a vertical components layout similar to vbox.

tab element attributes:

  • id – tab identifier. Please note that tabs are not components and their IDs are used only within a TabSheet in order to work with tabs from the controller.

  • caption – tab caption.

  • description - the text of a hint which is displayed in a popup when a user hovers the mouse cursor over or clicks on the concrete tab.

    gui tabsheet tab description
  • closable - defines whether the x button for closing the tab is displayed. Default value is false.

  • icon - defines icon location in theme catalog or the icon name in the icon set. Applicable only for the Web Client. Detailed information on recommended icon placement is available in Icons.

  • lazy – sets lazy loading for tab content.

    Lazy tabs do not load their content when the screen is opened, which reduces the number of components in memory. Components within a tab are loaded only when a user selects the tab. Additionally, if a lazy-tab includes visual components linked to a data container with a loader, the loader will not be triggered. As a result, screen opens faster, and data is loaded only when the user requests it by selecting this tab.

    Please note that the components located on a lazy tab do not exist when the screen is opened. Therefore they cannot be injected into a controller and cannot be obtained by invoking getComponent() in the controller’s init() method. The lazy tab components can be accessed only after the user opens the tab. This moment may be intercepted using TabSheet.SelectedTabChangeListener, for example:

    @Inject
    private TabSheet tabSheet;
    
    private boolean detailsInitialized, historyInitialized;
    
    @Subscribe
    protected void onInit(InitEvent event) {
        tabSheet.addSelectedTabChangeListener(selectedTabChangeEvent -> {
            if ("detailsTab".equals(selectedTabChangeEvent.getSelectedTab().getName())) {
                initDetails();
            } else if ("historyTab".equals(selectedTabChangeEvent.getSelectedTab().getName())) {
                initHistory();
            }
        });
    }
    
    private void initDetails() {
        if (detailsInitialized) {
            return;
        }
        detailsInitialized = true; (1)
    }
    
    private void initHistory() {
        if (historyInitialized) {
            return;
        }
        historyInitialized = true; (2)
    }
    1 use getComponentNN("comp_id") here to get tab’s components
    2 use getComponentNN("comp_id") here to get tab’s components

    By default, tabs are not lazy, which means that all their content is loaded when a screen is opened.

    The origin of the SelectedTabChangeEvent can be tracked using isUserOriginated() method.

    TabSheet styles

    In Web Client with a Halo-based theme, you can set predefined styles to the TabSheet container using the stylename attribute either in the XML descriptor or in the screen controller:

    <tabSheet stylename="framed">
        <tab id="mainTab" caption="Framed tab"/>
    </tabSheet>

    When setting a style programmatically, select one of the HaloTheme class constants with the TABSHEET_ prefix:

    tabSheet.setStyleName(HaloTheme.TABSHEET_COMPACT_TABBAR);
    • centered-tabs - centers the tabs inside the tab bar. Works best if all the tabs fit completely in the tab bar (i.e. no tab bar scrolling).

    • compact-tabbar - reduces the whitespace around the tabs in the tab bar.

    • equal-width-tabs - gives equal amount of space to all tabs in the tab bar (i.e. expand ratio == 1 for all tabs). The tab captions will be truncated if they do not fit into the tab. Tab scrolling will be disabled when this style is applied (all tabs will be visible at the same time).

    • framed - adds a border around the whole component as well as around individual tabs in the tab bar.

    • icons-on-top - displays tab icons on top of the tab captions (by default the icons are place on the left side of the caption).

    • only-selected-closeable - only the selected tab has the close button visible. Does not prevent closing the tab programmatically, it only hides the button from the end user.

    • padded-tabbar - adds a small amount of padding around the tabs in the tab bar, so that they don’t touch the outer edges of the component.

The appearance of the TabSheet component can be customized using SCSS variables with $cuba-tabsheet-* prefix. You can change these variables in the visual editor after creating a theme extension or a custom theme.



3.5.2.3. Screen Layout Rules

Below we explain how to properly place visual components and containers on your screens.

3.5.2.3.1. Positioning of Components
Size types

Component dimensions, width and height, can be of the following types:

  • Content-based - AUTO

  • Fixed (pixels) - 10px

  • Relative (percent) - 100%

screen layout rules 1
Content-dependent size

The component will take enough space to fit its content.

Examples:

  • For Label, the size is defined by text length.

  • For containers, the size is defined by the sum of all component sizes inside a container.

XML
<label width=AUTO/>
Java
label.setWidth(Component.AUTO_SIZE);

Components with content-dependent size will adjust their dimensions during screen layout initialization or when the content size is changed.

screen layout rules 2
Fixed size

Fixed size implies that the component dimensions will not change at runtime.

XML
<vbox width=320px height=240px/>
Java
vbox.setWidth(320px);
screen layout rules 3
Relative size

Relative size indicates the percentage of available space that will be occupied by the component.

XML
<label width=100%/>
Java
label.setWidth(50%);

Components with relative size will react to changes in the amount of the available space and adjust their actual size on the screen.

screen layout rules 4
Container specifics

By default, containers without the expand attribute provide equal space for all nested components. Exceptions: flowBox and htmlBox.

For example:

<layout>
    <button caption="Button"/>
    <button caption="Button"/>
</layout>
screen layout rules 7

Components and containers width and height are content-dependent by default. Some containers have different default dimensions:

Container Width Height

VBox

100%

AUTO

GroupBox

100%

AUTO

FlowBox

100%

AUTO

The root layout element is a vertical container (VBox), which has 100% width and height. The height can be AUTO in dialog mode.

Tabs within a TabSheet are VBox containers.

GroupBox component contains a VBox or an HBox, depending on the orientation property value.

Example of a container with content-based size:

<layout>
    <vbox>
        <button caption="Button"/>
        <button caption="Button"/>
    </vbox>
</layout>
screen layout rules 8

Example of a container with relative size:

<layout spacing="true">
    <groupBox caption="GroupBox"
              height="100%"/>
    <button caption="Button"/>
</layout>
screen layout rules 9

Here, layout, as well as vbox or hbox, provides equal space to all nested components, and groupBox has 100% height. In addition to that, groupBox has 100% width by default and takes all the available space.

Component specifics

It is recommended to set the absolute or relative height for Table and Tree. Otherwise, a table/tree can take unlimited size, if there are too many rows or nodes.

ScrollBox must have fixed or relative (but not AUTO) width and height. Components inside ScrollBox, positioned in the scrolling direction, may not have relative dimensions.

The following examples show the correct use of horizontal and vertical ScrollBox containers. If scrolling is required in both directions, both height and width must be set for the components (AUTO or absolute).

screen layout rules 5
The expand option

The container’s expand attribute allows specifying the component that will be given maximum available space.

The component specified in expand will have 100% size in the direction of the component expansion (vertically - for VBox, horizontally - for HBox). When container size is changed, the component will change its size accordingly.

<vbox expand="bigBox">
    <vbox id="bigBox"/>
    <label value="Label"/>
</vbox>
screen layout rules 6

expand works relatively to component expansion, for example:

<layout spacing="true"
        expand="groupBox">
    <groupBox id="groupBox"
              caption="GroupBox"
              width="200px"/>
    <button caption="Button"/>
</layout>
screen layout rules 10

In the following example, the auxiliary Label element (spacer) is used. Due to applied expand, it takes all the space left in the container.

<layout expand="spacer">
    <textField caption="Number"/>
    <dateField caption="Date"/>
    <label id="spacer"/>
    <hbox spacing="true">
        <button caption="OK"/>
        <button caption="Cancel"/>
    </hbox>
</layout>
screen layout rules 11
3.5.2.3.2. Margins and Spacing
Margin for screen borders

The margin attribute enables setting margins between container borders and nested components.

If margin is set to true, the margin is applied to all sides of the container.

<layout>
    <vbox margin="true" height="100%">
        <groupBox caption="Group"
                  height="100%"/>
    </vbox>
    <groupBox caption="Group"
              height="100%"/>
</layout>
screen layout rules 12

Margins can be also set for each individual side (Top, Right, Bottom, Left). The example of top and bottom margins:

<vbox margin="true,false,true,false">
Spacing between components

The spacing attribute indicates whether the space should be added between nested components in the direction of the container expansion.

screen layout rules 13

Spacing will work correctly in case some of the nested components become invisible, so you should not use margin to emulate spacing.

<layout spacing="true">
    <button caption="Button"/>
    <button caption="Button"/>
    <button caption="Button"/>
    <button caption="Button"/>
</layout>
screen layout rules 14
3.5.2.3.3. Alignment
Aligning components inside a container

Use the align attribute to align components within a container.

For example, here the label is located in the centre of the container:

<vbox height="100%">
    <label align="MIDDLE_CENTER"
           value="Label"/>
</vbox>
screen layout rules 15

Component with specified alignment should not have 100% size in alignment direction. The container should provide more space than required by the component. The component will be aligned within this space.

The example of alignment within available space:

<layout>
    <groupBox height="100%"
              caption="Group"/>
    <label align="MIDDLE_CENTER"
           value="Label"/>
</layout>
screen layout rules 16
3.5.2.3.4. Common Layout Mistakes
Common mistake 1. Setting relative size for a component within a container with content-based size

Example of incorrect layout with relative size:

screen layout rules 17

In this example, a label has 100% height, while the default height for VBox is AUTO, i.e. content-based.

Example of incorrect layout with expand:

screen layout rules 18

Expand implicitly sets relative 100% height for the label, which is not correct, just like in the example above. In such cases, the screen may not look as expected. Some components may disappear or have zero size. If you encounter any layout problems, check that relative sizes are specified correctly first of all.

Common mistake 2. Components inside a ScrollBox have 100% dimensions

Example of incorrect layout:

screen layout rules 19

As a result of such mistake, scroll bars in ScrollBox will not appear even if the size of nested components exceeds the scrolling area.

screen layout rules 20
Common mistake 3. Aligning components with insufficient space

Example of incorrect layout:

screen layout rules 21

In this example, HBox has content-dependent size, therefore the label alignment has no effect.

screen layout rules 22
3.5.2.4. Miscellaneous

This section describes different elements of the generic user interface that are related to visual components.

3.5.2.4.1. UiComponents

UiComponents is a factory which allows you to create UI components by name, class or type token.

If you create a component working with data, use a type token to get the component parameterized by the specific value type. For Label, TextField or DateField component, use type token constants like TextField.TYPE_INTEGER. When creating a component working with entities, like PickerField, LookupField or Table, use the static of() method to get the appropriate type token. For other components and containers, use the component class as the argument.

For example:

@Inject
private UiComponents uiComponents;

@Subscribe
protected void onInit(InitEvent event) {
    // components working with simple data types
    Label<String> label = uiComponents.create(Label.TYPE_STRING);
    TextField<Integer> amountField = uiComponents.create(TextField.TYPE_INTEGER);
    LookupField<String> stringLookupField = uiComponents.create(LookupField.TYPE_STRING);

    // components working with entities
    LookupField<Customer> customerLookupField = uiComponents.create(LookupField.of(Customer.class));
    PickerField<Customer> pickerField = uiComponents.create(PickerField.of(Customer.class));
    Table<OrderLine> table = uiComponents.create(Table.of(OrderLine.class));

    // other components and containers
    Button okButton = uiComponents.create(Button.class);
    VBoxLayout vBox = uiComponents.create(VBoxLayout.class);

    // ...
}
3.5.2.4.2. Formatter

Formatter should be used with read-only components, such as Label, Table column and similar. Editable components values, for example, TextField, should be formatted using the Datatype mechanism.

In an XML-descriptor of a screen, a component’s formatter can be defined in a nested formatter element. The element has a single attribute:

  • class − the name of a class implementing a com.haulmont.cuba.gui.components.Formatter

If formatter’s constructor class has a org.dom4j.Element, parameter, then it will receive an XML element, describing this formatter. This can be used to parameterize a formatter instance. For example, using a formatted string. Particularly, DateFormatter and NumberFormatter classes in the platform can take the format string from the format attribute. Example of using the component:

<column id="date">
    <formatter class="com.haulmont.cuba.gui.components.formatters.DateFormatter" format="yyyy-MM-dd HH:mm:ss"/>
</column>

Additionally, DateFormatter class also recognizes a type attribute, which can have a DATE or DATETIME value. In this case, formatting is done using the Datatype mechanism using a dateFormat or a dateTimeFormat string respectively. For example:

<column id="endDate">
    <formatter class="com.haulmont.cuba.gui.components.formatters.DateFormatter" type="DATE"/>
</column>

By default, DateFormatter displays the date and time in the server timezone. To show the current user’s timezone, set true for the useUserTimezone attribute of the formatter.

If a formatter is implemented as an internal class, it should be declared with a static modifier and its name should be separated by "$" for loading, for example:

<formatter class="com.sample.sales.gui.OrderBrowse$CurrencyFormatter"/>

Formatter can be assigned to a component not only using a screen XML-descriptor , but also programmatically – by submitting a formatter instance into a setFormatter() component.

An example of declaring a custom formatter and using it to format values in a table column:

public class CurrencyFormatter implements Function<BigDecimal, String> {

    @Override
    public String apply(BigDecimal bigDecimal) {
        return NumberFormat.getCurrencyInstance(Locale.getDefault()).format(bigDecimal);
    }
}
@Inject
private GroupTable<Order> ordersTable;

@Subscribe
public void onInit(InitEvent event) {
    Function currencyFormatter = new CurrencyFormatter();
    ordersTable.getColumn("totalPrice").setFormatter(currencyFormatter);
}
3.5.2.4.3. Presentations

The mechanism of presentations allows users to manage table settings.

gui presentations

Users can:

  • Save presentations with unique names. Table settings are automatically saved in an active presentation.

  • Edit and remove presentations.

  • Switch between presentations.

  • Set up a default presentation, which will be applied on the screen opening.

  • Create global presentations, available to all users. In order to create, change or remove global presentations, a user should have cuba.gui.presentations.global security permission.

Presentations are available to components implementing the com.haulmont.cuba.gui.components.Component.HasPresentations interface. These components are:

3.5.2.4.4. Validator

Validator is designed to check values entered into visual components.

Validation and input type checking should be differentiated. If a given component (e.g., TextField) data type is set to anything different than string (this can happen when binding to an entity attribute or setting datatype), then the component will not allow the user to enter a value that does not comply with this data type. When the component loses focus or when the user presses Enter, the error notification will be shown to the user.

On the other hand, validation does not act immediately on data entry or focus loss, but rather when the component’s validate() method is invoked. It means that the component (and the entity attribute that it is linked to) may temporarily contain a value, which does not comply with the conditions of validation. It should not be a problem because the validated fields are typically located in editor screens, which automatically invoke validation for all their fields before commit. If the component is located not in an edit screen, its validate() method should be invoked explicitly in the screen controller.

The framework contains the set of implementations for the most frequently used validators, which can be used in your project:

In a screen XML-descriptor, such component validators can be defined in a nested validators element. Validator can be added using CUBA Studio interface. Below is an example of adding a validator to the TextField component:

gui validator

Each validator is a Prototype Bean, and if you want to use validators from Java code, you need to get them using BeanLocator.

Some of the validators use Groovy string in error message. It means that it is possible to pass parameters to the error message (e.g., $value). These parameters take account of user’s locale.

You can use a custom Java class as a validator. It should implement the Consumer interface.

In a screen XML-descriptor, a custom validator can be defined in a nested validator element.

If the validator is implemented as an internal class, it should be declared with a static modifier, and its name should be separated by "$", for example:

<validator class="com.sample.sales.gui.AddressEdit$ZipValidator"/>

A validator class can be assigned to a component not only using a screen XML-descriptor but also programmatically – by submitting a validator instance into the component’s addValidator() method.

Example of creating a validator class for zip codes:

public class ZipValidator implements Consumer<String> {
    @Override
    public void accept(String s) throws ValidationException {
        if (s != null && s.length() != 6)
            throw new ValidationException("Zip must be of 6 characters length");
    }
}

Example of using a zip code validator for the TextField component:

<textField id="zipField" property="zip">
    <validator class="com.company.sample.web.ZipValidator"/>
</textField>

Example of setting a validator programmatically in a screen controller:

zipField.addValidator(value -> {
    if (value != null && value.length() != 6)
        throw new ValidationException("Zip must be of 6 characters length");
});

Below we consider predefined validators.

DecimalMaxValidator

It checks that value is less than or equal to the specified maximum. Supported types: BigDecimal, BigInteger, Long, Integer, and String that represents BigDecimal value with the current locale.

It has the following attributes:

  • value − maximum value (required);

  • inclusive − when set to true, the value should be less than or equal to the specified maximum value. The default value is true;

  • message − a custom message displayed to a user when validation fails. This message can contain $value and $max keys for formatted output.

Default message keys:

  • validation.constraints.decimalMaxInclusive

  • validation.constraints.decimalMax

Layout descriptor usage:

<textField id="numberField" property="numberProperty">
    <validators>
        <decimalMax value="10000" inclusive="false" message="Value '$value' cannot be greater than `$max`"/>
    </validators>
</textField>

Java code usage:

DecimalMaxValidator maxValidator = beanLocator.getPrototype(DecimalMaxValidator.NAME, new BigDecimal(100));
numberField.addValidator(maxValidator);
DecimalMinValidator

It checks that value is greater than or equal to the specified minimum. Supported types: BigDecimal, BigInteger, Long, Integer, and String that represents BigDecimal value with the current locale.

It has the following attributes:

  • value − minimum value (required);

  • inclusive − when set to true, the value should be greater than or equal to the specified minimum value. The default value is true;

  • message − a custom message displayed to a user when validation fails. This message can contain $value and $min keys for formatted output.

Default message keys:

  • validation.constraints.decimalMinInclusive

  • validation.constraints.decimalMin

Layout descriptor usage:

<textField id="numberField" property="numberProperty">
    <validators>
        <decimalMin value="100" inclusive="false" message="Value '$value' cannot be less than `$min`"/>
    </validators>
</textField>

Java code usage:

DecimalMinValidator minValidator = beanLocator.getPrototype(DecimalMinValidator.NAME, new BigDecimal(100));
numberField.addValidator(minValidator);
DigitsValidator

It checks that value is a number within the accepted range. Supported types: BigDecimal, BigInteger, Long, Integer, and String that represents BigDecimal value with the current locale.

It has the following attributes:

  • integer − count of numbers in the integer part (required);

  • fraction − count of numbers in the fraction part (required);

  • message − a custom message displayed to a user when validation fails. This message can contain $value, $integer, and $fraction keys for formatted output.

Default message keys:

  • validation.constraints.digits

Layout descriptor usage:

<textField id="numberField" property="numberProperty">
    <validators>
        <digits integer="3" fraction="2" message="Value '$value' is out of bounds ($integer digits are expected in integer part and $fraction in fractional part)"/>
    </validators>
</textField>

Java code usage:

DigitsValidator digitsValidator = beanLocator.getPrototype(DigitsValidator.NAME, 3, 2);
numberField.addValidator(digitsValidator);
FutureOrPresentValidator

It validates that date or time is in the future or present. It doesn’t use Groovy string, so there are no parameters you can pass to the error message. Supported types: java.util.Date, LocalDate, LocalDateTime, LocalTime, OffsetDateTime, OffsetTime.

It has the following attributes:

  • checkSeconds − when set to true, the validator should compare date or time with seconds and nanos. The default value is false;

  • message − a custom message displayed to a user when validation fails.

Default message keys:

  • validation.constraints.futureOrPresent

Layout descriptor usage:

<dateField id="dateTimePropertyField" property="dateTimeProperty">
    <validators>
        <futureOrPresent checkSeconds="true"/>
    </validators>
</dateField>

Java code usage:

FutureOrPresentValidator futureOrPresentValidator = beanLocator.getPrototype(FutureOrPresentValidator.NAME);
dateField.addValidator(futureOrPresentValidator);
FutureValidator

It validates that date or time is in the future. It doesn’t use Groovy string, so there are no parameters you can pass to the error message. Supported types: java.util.Date, LocalDate, LocalDateTime, LocalTime, OffsetDateTime, OffsetTime.

It has the following attributes:

  • checkSeconds − when set to true, the validator should compare date or time with seconds and nanos. The default value is false;

  • message − a custom message displayed to a user when validation fails.

Default message keys:

  • validation.constraints.future

Layout descriptor usage:

<timeField id="localTimeField" property="localTimeProperty" showSeconds="true">
    <validators>
        <future checkSeconds="true"/>
    </validators>
</timeField>

Java code usage:

FutureValidator futureValidator = beanLocator.getPrototype(FutureValidator.NAME);
timeField.addValidator(futureValidator);
MaxValidator

It checks that value is less than or equal to the specified maximum. Supported types: BigDecimal, BigInteger, Long, Integer.

It has the following attributes:

  • value − maximum value (required);

  • message − a custom message displayed to a user when validation fails. This message can contain $value and $max keys for formatted output.

Default message keys:

  • validation.constraints.max

Layout descriptor usage:

<textField id="numberField" property="numberProperty">
    <validators>
        <max value="20500" message="Value '$value' must be less than or equal to '$max'"/>
    </validators>
</textField>

Java code usage:

MaxValidator maxValidator = beanLocator.getPrototype(MaxValidator.NAME, 20500);
numberField.addValidator(maxValidator);
MinValidator

It checks that value is greater than or equal to the specified minimum. Supported types: BigDecimal, BigInteger, Long, Integer.

It has the following attributes:

  • value − minimum value (required);

  • message − a custom message displayed to a user when validation fails. This message can contain $value, and $min keys for formatted output.

Default message keys:

  • validation.constraints.min

Layout descriptor usage:

<textField id="numberField" property="numberProperty">
    <validators>
        <min value="30" message="Value '$value' must be greater than or equal to '$min'"/>
    </validators>
</textField>

Java code usage:

MinValidator minValidator = beanLocator.getPrototype(MinValidator.NAME, 30);
numberField.addValidator(minValidator);
NegativeOrZeroValidator

It checks that value is less than or equal 0. Supported types: BigDecimal, BigInteger, Long, Integer, Double, Float.

It has the following attributes:

  • message − a custom message displayed to a user when validation fails. This message can contain $value key for formatted output. Note, that Float doesn’t have its own datatype and won’t be formatted with the user’s locale.

Default message keys:

  • validation.constraints.negativeOrZero

Layout descriptor usage:

<textField id="numberField" property="numberProperty">
    <validators>
        <negativeOrZero message="Value '$value' must be less than or equal to 0"/>
    </validators>
</textField>

Java code usage:

NegativeOrZeroValidator negativeOrZeroValidator = beanLocator.getPrototype(NegativeOrZeroValidator.NAME);
numberField.addValidator(negativeOrZeroValidator);
NegativeValidator

It checks that value is strictly less than 0. Supported types: BigDecimal, BigInteger, Long, Integer, Double, Float.

It has the following attributes:

  • message − a custom message displayed to a user when validation fails. This message can contain $value key for formatted output. Note, that Float doesn’t have its own datatype and won’t be formatted with the user’s locale.

Default message keys:

  • validation.constraints.negative

Layout descriptor usage:

<textField id="numberField" property="numberProperty">
    <validators>
        <negative message="Value '$value' should be less than 0"/>
    </validators>
</textField>

Java code usage:

NegativeValidator negativeValidator = beanLocator.getPrototype(NegativeValidator.NAME);
numberField.addValidator(negativeValidator);
NotBlankValidator

It checks that value contains at least one non-whitespace character. It doesn’t use Groovy string, so there are no parameters you can pass to the error message. Supported type: String.

It has the following attributes:

  • message − a custom message displayed to a user when validation fails.

Default message keys:

  • validation.constraints.notBlank

Layout descriptor usage:

<textField id="textField" property="textProperty">
    <validators>
        <notBlank message="Value must contain at least one non-whitespace character"/>
    </validators>
</textField>

Java code usage:

NotBlankValidator notBlankValidator = beanLocator.getPrototype(NotBlankValidator.NAME);
textField.addValidator(notBlankValidator);
NotEmptyValidator

It checks that value is not null and not empty. Supported types: Collection and String.

It has the following attributes:

  • message − a custom message displayed to a user when validation fails. This message can contain $value key for formatted output, only for String type.

Default message keys:

  • validation.constraints.notEmpty

Layout descriptor usage:

<textField id="textField" property="textProperty">
    <validators>
        <notBlank message="Value must contain at least one non-whitespace character"/>
    </validators>
</textField>

Java code usage:

NotBlankValidator notBlankValidator = beanLocator.getPrototype(NotBlankValidator.NAME);
textField.addValidator(notBlankValidator);
NotNullValidator

It checks that value is not null. It doesn’t use Groovy string, so there are no parameters you can pass to the error message.

It has the following attributes:

  • message − a custom message displayed to a user when validation fails.

Default message keys:

  • validation.constraints.notNull

Layout descriptor usage:

<textField id="numberField" property="numberProperty">
    <validators>
        <notNull/>
    </validators>
</textField>

Java code usage:

NotNullValidator notNullValidator = beanLocator.getPrototype(NotNullValidator.NAME);
numberField.addValidator(notNullValidator);
PastOrPresentValidator

It validates that date or time is in the past or present. It doesn’t use Groovy string, so there are no parameters you can pass to the error message. Supported types: java.util.Date, LocalDate, LocalDateTime, LocalTime, OffsetDateTime, OffsetTime.

It has the following attributes:

  • checkSeconds − when setting to true, the validator should compare date or time with seconds and nanos. The default value is false;

  • message − a custom message displayed to a user when validation fails.

Default message keys:

  • validation.constraints.pastOrPresent

Layout descriptor usage:

<dateField id="dateTimeField" property="dateTimeProperty">
    <validators>
        <pastOrPresent/>
    </validators>
</dateField>

Java code usage:

PastOrPresentValidator pastOrPresentValidator = beanLocator.getPrototype(PastOrPresentValidator.NAME);
numberField.addValidator(pastOrPresentValidator);
PastValidator

It validates that date or time is in the past. It doesn’t use Groovy string, so there are no parameters you can pass to the error message. Supported types: java.util.Date, LocalDate, LocalDateTime, LocalTime, OffsetDateTime, OffsetTime.

It has the following attributes:

  • checkSeconds − when setting to true, the validator should compare date or time with seconds and nanos. The default value is false;

  • message − a custom message displayed to a user when validation fails.

Default message keys:

  • validation.constraints.past

Layout descriptor usage:

<dateField id="dateTimeField" property="dateTimeProperty">
    <validators>
        <pastOrPresent/>
    </validators>
</dateField>

Java code usage:

PastOrPresentValidator pastOrPresentValidator = beanLocator.getPrototype(PastOrPresentValidator.NAME);
numberField.addValidator(pastOrPresentValidator);
PositiveOrZeroValidator

It checks that value is greater than or equal to 0. Supported types: BigDecimal, BigInteger, Long, Integer, Double, Float.

It has the following attributes:

  • message − a custom message displayed to a user when validation fails. This message can contain $value key for formatted output. Note, that Float doesn’t have its own datatype and won’t be formatted with the user’s locale.

Default message keys:

  • validation.constraints.positiveOrZero

Layout descriptor usage:

<textField id="numberField" property="numberProperty">
    <validators>
        <positiveOrZero message="Value '$value' should be greater than or equal to '0'"/>
    </validators>
</textField>

Java code usage:

PositiveOrZeroValidator positiveOrZeroValidator = beanLocator.getPrototype(PositiveOrZeroValidator.NAME);
numberField.addValidator(positiveOrZeroValidator);
PositiveValidator

It checks that value is strictly greater than 0. Supported types: BigDecimal, BigInteger, Long, Integer, Double, Float.

It has the following attributes:

  • message − a custom message displayed to a user when validation fails. This message can contain $value key for formatted output. Note, that Float doesn’t have its own datatype and won’t be formatted with the user’s locale.

Default message keys:

  • validation.constraints.positive

Layout descriptor usage:

<textField id="numberField" property="numberProperty">
    <validators>
        <positive message="Value '$value' should be greater than '0'"/>
    </validators>
</textField>

Java code usage:

PositiveValidator positiveValidator = beanLocator.getPrototype(PositiveValidator.NAME);
numberField.addValidator(positiveValidator);
RegexpValidator

It checks that String value is matched with specified regular expression. Supported type: String.

It has the following attributes:

  • regexp − a regular expression to match (required);

  • message − a custom message displayed to a user when validation fails. This message can contain $value key for formatted output.

Default message keys:

  • validation.constraints.regexp

Layout descriptor usage:

<textField id="textField" property="textProperty">
    <validators>
        <regexp regexp="[a-z]*"/>
    </validators>
</textField>

Java code usage:

RegexpValidator regexpValidator = beanLocator.getPrototype(RegexpValidator.NAME, "[a-z]*");
textField.addValidator(regexpValidator);
SizeValidator

It checks that value is in a specific range. Supported types: Collection and String.

It has the following attributes:

  • min − a minimum value (with inclusive), cannot be less than 0. The default value is 0;

  • max − a maximum value (with inclusive), cannot be less than 0. The default value is Integer.MAX_VALUE;

  • message − a custom message displayed to a user when validation fails. This message can contain $value (only for String type), $min, $max keys for formatted output.

Default message keys:

  • validation.constraints.collectionSizeRange

  • validation.constraints.sizeRange

Layout descriptor usage:

<textField id="textField" property="textProperty">
    <validators>
        <size min="2" max="10" message="Value '$value' should be between '$min' and '$max'"/>
    </validators>
</textField>

<twinColumn id="twinColumn">
    <validators>
        <size min="2" max="4" message="Collection size must be between $min and $max"/>
    </validators>
</twinColumn>

Java code usage:

SizeValidator sizeValidator = beanLocator.getPrototype(SizeValidator.NAME);
textField.addValidator(sizeValidator);
3.5.2.5. API of Components
Common
  • unwrap() - returns client-specific component instance (Vaadin or Swing component). Can be used in client module to simplify invocation of underlying API, see Working with Vaadin Components section.

    com.vaadin.ui.TextField vTextField = textField.unwrap(com.vaadin.ui.TextField.class);
  • unwrapComposition() - returns the outmost external container of client-specific component instance. Can be used in client module to simplify invocation of underlying API.

Available for all components.

Buffered
  • commit() - updates all changes made since the previous commit to the data source.

  • discard() - discards all changes since last commit. The object updates its value from the data source.

  • isModified() - returns true if the object value has been modified since it was last updated from the data source.

if (textArea.isModified()) {
    textArea.commit();
}

Available for components:

Collapsable
  • addExpandedStateChangeListener() - adds the listener implementing the ExpandedStateChangeListener interface to intercept the component’s expanded state change events.

    @Subscribe("groupBox")
    protected void onGroupBoxExpandedStateChange(Collapsable.ExpandedStateChangeEvent event) {
        notifications.create()
                .withCaption("Expanded: " + groupBox.isExpanded())
                .show();
    }

    Available for components:

ComponentContainer
  • add() - adds child component to the container.

  • remove() - removes the child component from the container.

  • removeAll() - removes all children components from te container.

  • getOwnComponent() - returns the component directly owned by this container.

  • getComponent() - returns the component belonging to the whole components tree below this container.

  • getComponentNN() - returns the component belonging to the whole components tree below this container. Throws an exception if not found.

  • getOwnComponents() - returns all components directly owned by this container.

  • getComponents() - returns all components belonging to the whole components tree below this container.

Available for components:

OrderedContainer
  • indexOf() - returns the index of a given component in an ordered container.

Available for components:

HasContextHelp
  • setContextHelpText() - sets context help text. If set, then a special icon will be added for a field, see contextHelpText.

  • setContextHelpTextHtmlEnabled() - defines if context help text should be rendered as HTML, see contextHelpTextHtmlEnabled.

  • setContextHelpIconClickHandler() - sets a context help icon click handler. Click handler has priority over context help text, i.e. no tooltip with context help text will be shown if the click handler is set.

textArea.setContextHelpIconClickHandler(contextHelpIconClickEvent ->
        dialogs.createMessageDialog()
                .withCaption("Title")
                .withMessage("Message body")
                .withType(Dialogs.MessageType.CONFIRMATION)
                .show()
);

Available for almost all components:

HasSettings
  • applySettings() - restores the last user settings for this component.

  • saveSettings() - saves current user settings for this component.

Available for components:

HasUserOriginated
  • isUserOriginated() - provides information of the event origin. Returns true if this event was triggered by user interaction, on the client side, or false if it was triggered programmatically, on the server side.

    Usage example:

    @Subscribe("customersTable")
    protected void onCustomersTableSelection(Table.SelectionEvent<Customer> event) {
        if (event.isUserOriginated())
            notifications.create()
                    .withCaption("You selected " + event.getSelected().size() + " customers")
                    .show();
    }

The isUserOriginated() method is available for the following events:

HasValue
  • addValueChangeListener() - adds the listener implementing the ValueChangeListener interface to intercept the component’s value changes.

    @Inject
    private TextField<String> textField;
    @Inject
    private Notifications notifications;
    
    @Subscribe
    protected void onInit(InitEvent event) {
        textField.addValueChangeListener(stringValueChangeEvent ->
                notifications.create()
                        .withCaption("Before: " + stringValueChangeEvent.getPrevValue() +
                                ". After: " + stringValueChangeEvent.getValue())
                        .show());
    }

    For the same purpose, you can subscribe to a dedicated event of a component, for example:

    @Subscribe("textField")
    protected void onTextFieldValueChange(HasValue.ValueChangeEvent<String> event) {
        notifications.create()
                .withCaption("Before: " + event.getPrevValue() +
                        ". After: " + event.getValue())
                .show();
    }

See also UserOriginated.

Available for components:

LayoutClickNotifier
  • addLayoutClickListener() - adds the listener implementing the LayoutClickListener interface to intercept the clicks on the component area.

    vbox.addLayoutClickListener(layoutClickEvent ->
        notifications.create()
                .withCaption("Clicked")
                .show());

    For the same purpose, you can subscribe to a dedicated event of a component, for example:

    @Subscribe("vbox")
    protected void onVboxLayoutClick(LayoutClickNotifier.LayoutClickEvent event) {
        notifications.create()
                .withCaption("Clicked")
                .show();
    }

Available for components:

HasMargin
  • setMargin() - sets the margins for the component.

    • Sets margins on all sides of the component:

      vbox.setMargin(true);
    • Sets margins only on the top and the bottom of the component:

      vbox.setMargin(true, false, true, false);
    • Creates new instance of MarginInfo configuration class:

      vbox.setMargin(new MarginInfo(true, false, false, true));
  • getMargin() - returns margin configuration as an instance of MarginInfo class.

HasOuterMargin
  • setOuterMargin() - sets the outer margins outside the border of the component.

    • Sets outer margins on all sides of the component:

      groupBox.setOuterMargin(true);
    • Sets outer margins only on the top and the bottom of the component:

      groupBox.setOuterMargin(true, false, true, false);
    • Creates new instance of MarginInfo configuration class:

      groupBox.setOuterMargin(new MarginInfo(true, false, false, true));
  • getOuterMargin() - returns outer margin configuration as an instance of MarginInfo class.

Available for component:

HasSpacing
  • setSpacing() - adds space between the component and its child components.

    vbox.setSpacing(true);

Available for components:

ShortcutNotifier
  • addShortcutAction() - adds an action which is triggered when the user presses a given key combination.

    cssLayout.addShortcutAction(new ShortcutAction("SHIFT-A", shortcutTriggeredEvent ->
            notifications.create()
                    .withCaption("SHIFT-A action")
                    .show()));

Available for components:

3.5.2.6. XML-Attributes of Components
align

Defines the component position relative to the parent container. Possible values are:

  • TOP_RIGHT

  • TOP_LEFT

  • TOP_CENTER

  • MIDDLE_RIGHT

  • MIDDLE_LEFT

  • MIDDLE_CENTER

  • BOTTOM_RIGHT

  • BOTTOM_LEFT

  • BOTTOM_CENTER

box.expandRatio

In vbox and hbox containers, components are placed in slots. The box.expandRatio attribute specifies the expand ratio for each slot. The ratio must be greater than or equal to 0.

<hbox width="500px" expand="button1" spacing="true">
    <button id="button1" box.expandRatio="1"/>
    <button id="button2" width="100%" box.expandRatio="3"/>
    <button id="button3" width="100%" box.expandRatio="2"/>
</hbox>

If we specify box.expandRatio=1 to one component and its height or width is 100% (depends on layout), this component will be expanded to use all available space in the direction of component placement.

By default, all slots for components have equal width or height (i.e. box.expandRatio = 1). If another value is set for at least one component, all implicit values are ignored, and only explicitly assigned values are considered.

See also the expand attribute.

caption

Sets the component’s caption.

The attribute value can either be a message or a key in a message pack. In case of a key, the value should begin with the msg:// prefix.

There are two ways of setting a key:

  • A short key – in this case the message will be searched in the package of the current screen:

    caption="msg://infoFieldCaption"
  • Full key including package name:

    caption="msg://com.company.sample.gui.screen/infoFieldCaption"
captionAsHtml

Defines whether HTML is enabled in the component’s caption. If set to true, the captions are rendered in the browser as HTML, and the developer is responsible for ensuring no harmful HTML is used. If set to false, the content is rendered in the browser as plain text.

Possible values − true, false. Default is false.

captionProperty

Defines the name of an entity attribute which is displayed by a component. captionProperty can only be used for entities contained in a datasource (for example, defined by the optionsDatasource property of the LookupField component).

If captionProperty is not defined, instance name is shown.

colspan

Sets the number of grid columns that the component should occupy (default is 1).

This attribute can be defined for any component located immediately within a GridLayout container.

contextHelpText

Sets the context help text. If set, then a special ? icon will be added for a field. If the field has an external caption, i.e. either caption or icon attribute is set, then the context help icon will be displayed next to the caption text, otherwise next to the field itself:

gui attr contextHelpIcon

In the web client, the context help tooltip appears when the users hovers over the ? icon.

<textField id="textField"
           contextHelpText="msg://contextHelp"/>
gui attr contextHelp
contextHelpTextHtmlEnabled

Defines if context help text can be presented as HTML.

<textField id="textField"
           description="Description"
           contextHelpText="<p><h1>Lorem ipsum dolor</h1> sit amet, <b>consectetur</b> adipiscing elit.</p><p>Donec a lobortis nisl.</p>"
           contextHelpTextHtmlEnabled="true"/>
gui attr contextHelpHtml

Possible values − true, false.

css

Provides a declarative way to set CSS properties for UI components. This attribute can be used together with the stylename attribute, see an example below.

XML definition:
<cssLayout css="display: grid; grid-gap: 10px; grid-template-columns: 33% 33% 33%"
           stylename="demo"
           width="100%"
           height="100%">
    <label value="A" css="grid-column: 1 / 3; grid-row: 1"/>
    <label value="B" css="grid-column: 3; grid-row: 1 / 3;"/>
    <label value="C" css="grid-column: 1; grid-row: 2;"/>
    <label value="D" css="grid-column: 2; grid-row: 2;"/>
</cssLayout>
Additional CSS:
  .demo > .v-label {
    display: block;
    background-color: #444;
    color: #fff;
    border-radius: 5px;
    padding: 20px;
    font-size: 150%;
  }
dataContainer

Sets a data container defined in the data section of the screen XML descriptor.

When setting the dataContainer attribute for a component, the property attribute should also be set.

dataLoader

Sets a data loader defined for a data container in the data section of the screen XML descriptor.

datasource

Sets a data source defined in the dsContext section of the screen XML descriptor.

When setting the datasource attribute for a component implementing the DatasourceComponent interface, the property attribute should also be set.

datatype

Sets a data type if the field is not connected to an entity attribute (i.e. the data container and attribute name are not set). The attribute value accepts a data type registered in the application metadata − see Datatype.

The attribute is used for TextField, DateField, DatePicker, TimeField components.

description

Defines a hint which is displayed in a popup when a user hovers the mouse cursor over or clicks on the component area.

descriptionAsHtml

Defines whether HTML is allowed in the component’s description. If set to true, the captions are rendered in the browser as HTML, and the developer is responsible for ensuring no harmful HTML is used. If set to false, the content is rendered in the browser as plain text.

Possible values − true, false. Default is false.

editable

Indicates that the component’s content can be edited (do not confuse with enable).

Possible values − true, false. Default value is true.

Ability to edit content of a component bound to data (inheritor of DatasourceComponent or ListComponent) is also influenced by the security subsystem. If the security subsystem information indicates that the component should not be editable, the value of its editable attribute is ignored.

enable

Defines the component’s enabled/disabled state.

If a component is disabled, it does not accept input focus. Disabling a container disables all of its components as well. Possible values are true, false. By default all components are enabled.

expand

Defines a component within the container that should be expanded to use all available space in the direction of component placement. For a container with vertical placement, this attribute sets 100% height to a component; for the containers with horizontal placement - 100% width. Additionally, resizing a container will resize the expanded component. See also box.expandRatio.

height

Sets the component’s height. Can be set in pixels or in percents of the parent container height. For example: 100px, 100%, 50. If it is specified without units, pixels are assumed.

Setting a value in % means that the component will occupy the corresponding height within an area provided by the parent container.

When set to AUTO or -1px, a default value will be used for the component height. For a container, default height is defined by the content: it is the sum of the heights of all nested components.

icon

Sets a component icon.

The attribute value should contain a path to an icon file relative to the themes folder:

icon="icons/create.png"

or the icon name in an icon set:

icon="CREATE_ACTION"

If different icons should be displayed depending on the user’s language, you can set paths to the icons in the message pack and specify a message key in the icon attribute, for example:

icon="msg://addIcon"

Font elements of Font Awesome can be used instead of files in web client with Halo theme (or derived from it). For this, specify the name of the required constant of the com.vaadin.server.FontAwesome class in the icon property with the font-icon: prefix, for example:

icon="font-icon:BOOK"

For more details on the usage of icons see the Icons section.

id

Sets an identifier of the component.

It is recommended to create identifiers according to the rules for Java-identifiers and use camelСase, for example: userGrid, filterPanel. The id attribute can be specified for any component and should be unique within a screen.

inputPrompt

Defines a string which is displayed in the field when its value is null.

<suggestionField inputPrompt="Let's search something!"/>

The attribute is used for TextField, LookupField, LookupPickerField, SearchPickerField, SuggestionPickerField components in web client only.

margin

Defines indentation between the outer borders and the container content.

It can take value of two types:

  • margin="true" − enables margins for all sides.

  • margin="true,false,true,false" − enables only the top and the bottom margin (the value format is "top,right,bottom,left").

By default margins are disabled.

nullName

Selection of the option defined in the nullName attribute is equal to setting the null value to the component.

The attribute is used for LookupField, LookupPickerField, and SearchPickerField components.

Example of setting an attribute value in an XML-descriptor:

<lookupField datasource="orderDs"
             property="customer"
             nullName="(none)"
             optionsDatasource="customersDs" width="200px"/>

Example of setting an attribute value in a controller:

<lookupField id="customerLookupField" optionsDatasource="customersDs"
             width="200px" datasource="orderDs" property="customer"/>
customerLookupField.setNullOption("<null>");
openType

Defines how a related screen will be opened. Corresponds to the WindowManager.OpenType enumeration with the values NEW_TAB, THIS_TAB, NEW_WINDOW, DIALOG. Default value is THIS_TAB.

optionsContainer

Sets the name of a data container which contains a list of options.

captionProperty attribute can be used together with optionsContainer.

optionsDatasource

Sets the name of a data source which contains a list of options.

captionProperty attribute can be used together with optionsDatasource.

optionsEnum

Sets the enumeration class name which contains a list of options.

property

Sets the name of an entity attribute which value will be displayed and edited by this visual component.

property is always used together with the datasource attribute.

required

Indicates that this field requires a value.

Possible values − true, false. Default is false.

The requiredMessage attribute can be used together with required.

requiredMessage

Used together with the required attribute. It sets a message that will be displayed to a user when the component has no value.

The attribute can contain a message or a key from a message pack, for example: requiredMessage="msg://infoTextField.requiredMessage"

responsive

Indicates that the component should react on change in the available space. Reaction can be customized with the help of styles.

Possible values − true, false. Default is false.

rowspan

Sets the number of grid lines that the component should occupy (default is 1).

This attribute can be set for any component located immediately within a GridLayout container.

settingsEnabled

Defines if user settings for the component should be saved/restored. Settings are saved only if the component’s id is set.

Possible values − true, false. Default is true.

spacing

Sets spacing between components within a container.

Possible values − true, false.

By default spacing is disabled.

stylename

Defines a style name for a component. See Themes for details.

There are several predefined styles in halo theme available for the components:

  • huge - sets the field size to 160% of its default size.

  • large - sets the field size to 120% of its default size.

  • small - sets the field size to 85% of its default size.

  • tiny - sets the field size to 75% of its default size.

tabCaptionsAsHtml

Defines whether HTML is allowed in the tab captions. If set to true, the captions are rendered in the browser as HTML, and the developer is responsible for ensuring no harmful HTML is used. If set to false, the content is rendered in the browser as plain text.

Possible values − true, false. Default is false.

tabIndex

Specifies whether the component is focusable and sets the relative order of the component in the sequence of focusable components on the screen.

It can can take integer values of positive or negative range:

  • negative value means that the component should be focusable, but should not be reachable via sequential keyboard navigation;

  • 0 means that the component should be focusable and reachable via sequential keyboard navigation, but its relative order follows its relative position on the screen;

  • positive value means the component should be focusable and reachable via sequential keyboard navigation; its relative order is defined by the value of the attribute: the sequential follows the increasing number of the tabIndex. If several components share the same tabIndex value, their relative order follows their relative position on the screen.

tabsVisible

Sets whether the tab selection part should be shown in the UI.

Possible values − true, false. Default is true.

textSelectionEnabled

Defines if text selection is enabled in table cells.

Possible values − true, false. Default is false.

visible

Sets visibility of the component. Possible values − true, false.

If a container is invisible all its components are invisible. By default all components are visible.

width

Defines component’s width.

The value can be set in pixels or in percents of the width of the parent container. For example: 100px, 100%, 50. If specified without units, pixels are assumed. Setting a value in % means that the component will occupy the corresponding width within an area provided by the parent container.

When set to AUTO or -1px, a default value will be used for a component width. For a container, the default width is defined by the content: it is the sum of the widths of all nested components.

3.5.3. Data Components

Data components are non-visual elements of screens that provide loading of data from the middle tier, binding it to data-aware visual components and saving changed data back to the middle tier. There are the following categories of data components:

  • Containers provide the thin layer between entities and data-aware visual components. Different types of containers hold either single instances or collections of entities.

  • Loaders load data from the middle tier to containers.

  • DataContext tracks changes in entities and saves changed instances back to the middle tier upon request.

Usually, data components are defined in the screen XML descriptor in the <data> element. They can be injected into the controller in the same way as visual components:

@Inject
private CollectionLoader<Customer> customersDl;

private String customerName;

@Subscribe
protected void onBeforeShow(BeforeShowEvent event) {
    customersDl.setParameter("name", customerName)
    customersDl.load();
}

Data components of a particular screen are registered in the ScreenData object which is associated with the screen controller and available through its getScreenData() method. This object is useful when you need to load all data for the screen, for example:

@Subscribe
protected void onBeforeShow(BeforeShowEvent event) {
    getScreenData().loadAll();
}

Please note that screens load data automatically when the @LoadDataBeforeShow annotation is present on the controller class. So programmatic loading is needed only when there is no such anntotation or the annotation value is set to false. It is usually required when you need to set some loading parameters, as in the example above.

3.5.3.1. Data Containers

Data containers form a thin layer between visual components and the data model. They are designed to hold entity instances and collections, provide information about entity meta-class, view and a selected instance for collections, register listeners to various events.

containers
Figure 25. Interfaces of Data Containers
3.5.3.1.1. InstanceContainer

The InstanceContainer interface is a root of data containers hierarchy. It is designed to hold a single entity instance and has the following methods:

  • setItem() - sets an entity instance to the container.

  • getItem() - returns the instance stored in the container. If the container is empty, the method throws an exception. Use this method when you are sure that an entity has been set in the container, then you don’t have to check the returned value for null.

  • getItemOrNull() - returns the instance stored in the container. If the container is empty, this method returns null. Always check the returned value for null before using it.

  • getEntityMetaClass() - returns the meta-class of the entity that can be stored in this container.

  • setView() - sets a view that must be used when loading entities for this container. Keep in mind that containers themselves do not load data, so this attribute just indicates the desired view for a loader connected to this container.

  • getView() - returns a view that must be used when loading entities for this container.

InstanceContainer events

The InstanceContainer interface allows you to register listeners to the following events.

  • ItemPropertyChangeEvent is sent when the value of an attribute of the instance stored in the container is changed. Example of subscribing to the event for a container defined in the screen XML with customerDc id:

    @Subscribe(id = "customerDc", target = Target.DATA_CONTAINER)
    private void onCustomerDcItemPropertyChange(
            InstanceContainer.ItemPropertyChangeEvent<Customer> event) {
        Customer customer = event.getItem();
        String changedProperty = event.getProperty();
        Object currentValue = event.getValue();
        Object previousValue = event.getPrevValue();
        // ...
    }
  • ItemChangeEvent is sent when another instance (or null) is set in the container. Example of subscribing to the event for a container defined in the screen XML with customerDc id:

    @Subscribe(id = "customerDc", target = Target.DATA_CONTAINER)
    private void onCustomerDcItemChange(InstanceContainer.ItemChangeEvent<Customer> event) {
        Customer customer = event.getItem();
        Customer previouslySelectedCustomer = event.getPrevItem();
        // ...
    }
3.5.3.1.2. CollectionContainer

The CollectionContainer interface is designed to hold a collection of entities of the same type. It is a descendant of InstanceContainer and defines the following specific methods:

  • setItems() - sets a collection of entities to the container.

  • getItems() - returns an immutable list of entities stored in the container. Use this method to iterate over the collection, to get a stream or to get an instance by its position in the list. If you need to get an entity instance by its id, use getItem(entityId). For example:

    @Inject
    private CollectionContainer<Customer> customersDc;
    
    private Optional<Customer> findByName(String name) {
        return customersDc.getItems().stream()
                .filter(customer -> Objects.equals(customer.getName(), name))
                .findFirst();
    }
  • getMutableItems() - returns a mutable list of entities stored in the container. All list changes caused by the add(), addAll(), remove(), removeAll(), set(), clear() methods produce CollectionChangeEvent, so subscribed visual components will update accordingly. For example:

    @Inject
    private CollectionContainer<Customer> customersDc;
    
    private void createCustomer() {
        Customer customer = metadata.create(Customer.class);
        customer.setName("Homer Simpson");
        customersDc.getMutableItems().add(customer);
    }

    Use getMutableItems() only when you want to change the collection, otherwise prefer getItems().

  • setItem() - sets the current item for this container. If the provided item is not null, it must exist in the collection. The method sends ItemChangeEvent.

    Please note that visual components like Table do not listen to ItemChangeEvent sent by the container. So if you want to select a row in a table, use the setSelected() method of the table instead of setItem() method of the collection container. The current item of the container will also be changed, because the container listens to the component. For example:

    @Inject
    private CollectionContainer<Customer> customersDc;
    @Inject
    private GroupTable<Customer> customersTable;
    
    private void selectFirstRow() {
        customersTable.setSelected(customersDc.getItems().get(0));
    }
  • getItem() - overrides the method of InstanceContainer and returns the current item. If the current item is not set, the method throws an exception. Use this method when you are sure that the current item has been set for the container, then you don’t have to check the returned value for null.

  • getItemOrNull() - overrides the method of InstanceContainer and returns the current item. If the current item is not set, this method returns null. Always check the returned value for null before using it.

  • getItemIndex(entityId) - returns the position of an instance with the given id in the list returned by the getItems() and getMutableItems() methods. This method accepts Object and you can pass either id or the entity instance itself. The container implementation maintains a map of ids to indexes, so the method works fast even on large lists.

  • getItem(entityId) - returns an instance from the collection by its id. It’s a shortcut method which first obtains the instance position using getItemIndex(entityId) and then returns the instance from the list using getItems().get(index). The method throws an exception if the instance with the specified id doesn’t exist in the collection.

  • getItemOrNull(entityId) - same as getItem(entityId) but returns null if the instance with the specified id doesn’t exist in the collection. Always check the returned value for null before using it.

  • containsItem(entityId) - returns true if an instance with the specified id exists in the collection. It’s a shortcut method which uses getItemIndex(entityId) under the hood.

  • replaceItem(entity) - if the item with the same id exists in the container, it is replaced with the given instance. If not, the given instance is added to the items list. The method sends CollectionChangeEvent of the SET_ITEM or ADD_ITEMS type depending on what has been done.

  • setSorter() - sets the given sorter for this container. Standard implementation of the Sorter interface is CollectionContainerSorter. It is set by the framework automatically when the container is associated with a loader. You can provide your own implementation if needed.

  • getSorter() - returns the sorter currently set for this container.

CollectionContainer events

In addition to events of InstanceContainer, the CollectionContainer interface allows you to register listeners to the CollectionChangeEvent event which is sent when the container items collection is changed, i.e. on adding, removing and replacing elements. Example of subscribing to the event for a container defined in the screen XML with customersDc id:

@Subscribe(id = "customersDc", target = Target.DATA_CONTAINER)
private void onCustomersDcCollectionChange(
        CollectionContainer.CollectionChangeEvent<Customer> event) {
    CollectionChangeType changeType = event.getChangeType(); (1)
    Collection<? extends Customer> changes = event.getChanges(); (2)
    // ...
}
1 - type of changes: REFRESH, ADD_ITEMS, REMOVE_ITEMS, SET_ITEM.
2 - collection of entities that were added or removed from the container. If the change type is REFRESH, the framework cannot determine what exactly items were added or removed, so this collection is empty.
3.5.3.1.3. Property Containers

InstancePropertyContainer and CollectionPropertyContainer are designed to work with entity instances and collections that are attributes of other entities. For example, if the Order entity that has the orderLines attribute which is a collection of the OrderLine entity, you can use CollectionPropertyContainer to bind orderLines to a table component.

Property containers implement the Nested interface which defines methods to get the master container and the name of its attribute to bind the property container to. In the example with Order and OrderLine entities, the master container is the one storing the Order instance.

InstancePropertyContainer works directly with the attribute of the master entity. It means that if you invoke its setItem() method, the value will be set to the corresponding master entity attribute and its ItemPropertyChangeEvent listener will be invoked.

CollectionPropertyContainer contains a copy of the master collection and its methods behave as follows:

  • getMutableItems() returns the mutable list of entities and changes in the list are reflected in the underlying property. That is if you remove an item from this list, the master attribute will be changed and ItemPropertyChangeEvent listener will be invoked on the master container.

  • getDisconnectedItems() returns the mutable list of entities, but changes in the list are not reflected in the underlying property. That is if you remove an item from this list, the master attribute will stay the same.

  • setItems() sets a collection of entities to the container and to the underlying property. ItemPropertyChangeEvent listener is invoked on the master container.

  • setDisconnectedItems() sets a collection of entities to the container, but the underlying master attribute will stay the same.

The getDisconnectedItems() and setDisconnectedItems() methods can be used to temporarily change the representation of the collection in UI, for example to filter a table:

@Inject
private CollectionPropertyContainer<OrderLine> orderLinesDc;

private void filterByProduct(String product) {
    orderLinesDc.getDisconnectedItems().removeIf(
            orderLine -> !orderLine.getProduct().equals(product));
}

private void resetFilter() {
    orderLinesDc.setDisconnectedItems(getEditedEntity().getOrderLines());
}
3.5.3.1.4. KeyValue Containers

KeyValueContainer and KeyValueCollectionContainer are designed to work with KeyValueEntity. This entity can contain an arbitrary number of attributes which are defined at runtime.

The KeyValue containers define the following specific methods:

  • addProperty() - as the container can store entities with any number of attributes, you have to specify what attributes are expected by using this method. It accepts a name of the attribute and its type in the form of Datatype or a Java class. In the latter case, the class should be either an entity class or a class supported by one of the datatypes.

  • setIdName() is an optional method which allows you to define one of the attributes as an identifier attribute of the entity. It means that KeyValueEntity instances stored in this container will have identifiers obtained from the given attribute. Otherwise, KeyValueEntity instances get randomly generated UUIDs.

  • getEntityMetaClass() returns a dynamic implementation of the MetaClass interface that represents the current schema of KeyValueEntity instances. It is defined by previous calls to addProperty().

3.5.3.2. Data Loaders

Loaders are designed to load data from the middle tier to containers.

There are slightly different interfaces of loaders depending on containers they work with:

  • InstanceLoader loads a single instance to InstanceContainer by entity id or JPQL query.

  • CollectionLoader loads a collection of entities to CollectionContainer by a JPQL query. You can specify paging, sorting and other optional parameters.

  • KeyValueCollectionLoader loads a collection of KeyValueEntity instances to KeyValueCollectionContainer. In addition to CollectionLoader parameters, you can specify a data store name.

In screen XML descriptors, all loaders are defined by the same <loader> element and the type of a loader is determined by what container it is enclosed in.

Loaders are optional because you can just load data using DataManager or your custom service and set directly to containers, but they simplify this process in declaratively defined screens, especially with the Filter component. Usually, a collection loader obtains a JPQL query from the screen XML descriptor and query parameters from the filter component, creates LoadContext and invokes DataManager to load entities. So the typical XML descriptor looks like this:

<data>
    <collection id="customersDc" class="com.company.sample.entity.Customer" view="_local">
        <loader id="customersDl">
            <query>
                select e from sample_Customer e
            </query>
        </loader>
    </collection>
</data>
<layout>
    <filter id="filter" applyTo="customersTable" dataLoader="customersDl">
        <properties include=".*"/>
    </filter>
    <!-- ... -->
</layout>

Attributes of the loader XML element allow you to define optional parameters like cacheable, softDeletion, etc.

In an entity editor screen, the loader XML element is usually empty, because the instance loader requires an entity identifier which is specified programmatically by the StandardEditor base class:

<data>
    <instance id="customerDc" class="com.company.sample.entity.Customer" view="_local">
        <loader/>
    </instance>
</data>

Loaders can delegate actual loading to a function which can be provided using the setLoadDelegate() method or declaratively using the @Install annotation in the screen controller, for example:

@Inject
private DataManager dataManager;

@Install(to = "customersDl", target = Target.DATA_LOADER)
protected List<Customer> customersDlLoadDelegate(LoadContext<Customer> loadContext) {
    return dataManager.loadList(loadContext);
}

In the example above, the customersDlLoadDelegate() method will be used by the customersDl loader to load the list of Customer entities. The method accepts LoadContext which will be created by the loader based on its parameters: query, filter (if any), etc. In the example, the loading is done via DataManager which is effectively the same as the standard loader implementation, but you can use a custom service or perform any post-processing of the loaded entities.

You can listen to PreLoadEvent and PostLoadEvent to add some logic before or after loading:

@Subscribe(id = "customersDl", target = Target.DATA_LOADER)
private void onCustomersDlPreLoad(CollectionLoader.PreLoadEvent<Customer> event) {
    // do something before loading
}

@Subscribe(id = "customersDl", target = Target.DATA_LOADER)
private void onCustomersDlPostLoad(CollectionLoader.PostLoadEvent<Customer> event) {
    // do something after loading
}

A loader can also be created and configured programmatically, for example:

@Inject
private DataComponents dataComponents;

private void createCustomerLoader(CollectionContainer<Customer> container) {
    CollectionLoader<Customer> loader = dataComponents.createCollectionLoader();
    loader.setQuery("select e from sample_Customer e");
    loader.setContainer(container);
    loader.setDataContext(getScreenData().getDataContext());
}

When DataContext is set for a loader (which is always the case when the loader is defined in XML descriptor), all loaded entities are automatically merged into the data context.

Query conditions

Sometimes you need to modify a data loader query at runtime to filter the loaded data at the database level. The simplest way to provide filtering based on parameters entered by users is to connect the Filter visual component to the data loader.

Instead of the universal filter or in addition to it, you can create a set of conditions for the loader query. A condition is a set of query fragments with parameters. These fragments are added to the resulting query text only when all parameters used in the fragments are set for the query. Conditions are processed on the data store level, so they can contain fragments of different query languages supported by data stores. The framework provides conditions for JPQL.

Let’s consider creating a set of conditions for filtering a Customer entity by two of its attributes: string name and boolean status.

Loader query conditions can be defined either declaratively in the <condition> XML element, or programmatically using the setCondition() method. Below is an example of configuring the conditions in XML:

<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd"
        xmlns:c="http://schemas.haulmont.com/cuba/screen/jpql_condition.xsd" (1)
        caption="Customers browser" focusComponent="customersTable">
    <data>
        <collection id="customersDc"
                    class="com.company.demo.entity.Customer" view="_local">
            <loader id="customersDl">
                <query><![CDATA[select e from demo_Customer e]]>
                    <condition> (2)
                        <and> (3)
                            <c:jpql> (4)
                                <c:where>e.name like :name</c:where>
                            </c:jpql>
                            <c:jpql>
                                <c:where>e.status = :status</c:where>
                            </c:jpql>
                        </and>
                    </condition>
                </query>
            </loader>
        </collection>
    </data>
1 - add the JPQL conditions namespace
2 - define the condition element inside query
3 - if you have more than one condition, add and or or element
4 - define a JPQL condition with optional join element and mandatory where element

Suppose that the screen has two UI components for entering the condition parameters: nameFilterField text field and statusFilterField check box. In order to refresh the data when a user changes their values, add the following event listeners to the screen controller:

@Inject
private CollectionLoader<Customer> customersDl;

@Subscribe("nameFilterField")
private void onNameFilterFieldValueChange(HasValue.ValueChangeEvent<String> event) {
    if (event.getValue() != null) {
        customersDl.setParameter("name", "(?i)%" + event.getValue() + "%"); (1)
    } else {
        customersDl.removeParameter("name");
    }
    customersDl.load();
}

@Subscribe("statusFilterField")
private void onStatusFilterFieldValueChange(HasValue.ValueChangeEvent<Boolean> event) {
    if (event.getValue()) {
        customersDl.setParameter("status", true);
    } else {
        customersDl.removeParameter("status");
    }
    customersDl.load();
}
1 - notice how we use Case-Insensitive Substring Search provided by ORM

As mentioned above, a condition is included in the query only when its parameters are set. So the resulting query executed on the database will depend on what is entered in the UI components:

Only nameFilterField has a value
select e from demo_Customer e where e.name like :name
Only statusFilterField has a value
select e from demo_Customer e where e.status = :status
Both nameFilterField and statusFilterField have values
select e from demo_Customer e where (e.name like :name) and (e.status = :status)
3.5.3.3. DataContext

DataContext is an interface for tracking changes in entities loaded to the client tier. Tracked entities are marked as "dirty" on any modification of their attributes, and DataContext saves dirty entities to the middle tier when its commit() method is invoked.

Within DataContext, an entity with the given identifier is represented by a single object instance, no matter where and how many times it is used in object graphs.

In order to be tracked, an entity must be put into DataContext using its merge() method. If the context does not contain the entity with the same id, the context creates a new instance, copies the state of the passed instance to the new one and returns it. If the context already contains an instance with the same id, it copies the state of the passed instance to the existing one and returns it. This mechanism allows the context to always have only one instance of an entity with a particular identifier.

When you merge an entity, the whole object graph with the root in this entity will be merged. I.e. all referenced entities (including collections) will become tracked.

The main rule of using the merge() method is to continue working with the returned instance and discarding the passed one. In most cases, the returned object instance will be different. The only exception is when you pass to merge() an instance which was earlier returned from another invocation of merge() or find() of the same context.

Example of merging an entity into DataContext:

@Inject
private DataContext dataContext;

private void loadCustomer(Id<Customer, UUID> customerId) {
    Customer customer = dataManager.load(customerId).one();
    Customer trackedCustomer = dataContext.merge(customer);
    customersDc.getMutableItems().add(trackedCustomer);
}

A single instance of DataContext exists for a given screen and all its nested fragments. It is created if the <data> element exists in the screen XML descriptor.

The <data> element can have readOnly="true" attribute, in that case a special "no-op" implementation is used which actually doesn’t track entities and hence doesn’t affect performance. By default, entity browsers scaffolded by Studio have the read-only data context, so if you need to track changes and commit dirty entities in a browser, remove the readOnly="true" XML attribute.

Parent DataContext

DataContext instances can form parent-child relationships. If a DataContext instance has parent context, it commits changed entities to the parent instead of saving them to the middle tier. This feature enables editing compositions, when detail entities are saved only together with the master entity. If an entity attribute is annotated with @Composition, the framework automatically sets parent context in the attribute editor screen, so the changed attribute entity will be saved to the data context of the master entity.

You can easily provide the same behavior for any entities and screens.

If you open an edit screen which should commit data to the current screen’s data context, use withParentDataContext() method of the builder:

@Inject
private ScreenBuilders screenBuilders;
@Inject
private DataContext dataContext;

private void editFooWithCurrentDataContextAsParent() {
    FooEdit fooEdit = screenBuilders.editor(Foo.class, this)
            .withScreenClass(FooEdit.class)
            .withParentDataContext(dataContext)
            .build();
    fooEdit.show();
}

If you open a simple screen using the Screens bean, provide a setter method accepting the parent data context:

public class FooScreen extends Screen {

    @Inject
    private DataContext dataContext;

    public void setParentDataContext(DataContext parentDataContext) {
        dataContext.setParent(parentDataContext);
    }
}

And use it after creating the screen:

@Inject
private Screens screens;
@Inject
private DataContext dataContext;

private void openFooScreenWithCurrentDataContextAsParent() {
    FooScreen fooScreen = screens.create(FooScreen.class);
    fooScreen.setParentDataContext(dataContext);
    fooScreen.show();
}

Make sure the parent data context is not defined with readOnly="true" attribute. Otherwise you will get an exception when try to use it as a parent for another context.

3.5.3.4. Using Data Components

In this section, we provide practical examples of working with data components.

3.5.3.4.1. Declarative Creation of Data Components

The simplest way to create data components for a screen is to define them in the screen XML descriptor in the <data> element.

Let’s consider the data model consisting of Customer, Order and OrderLine entities. The edit screen for the Order entity can have the following XML definition:

<data>
    <instance id="orderDc" class="com.company.sales.entity.Order" view="order-edit">
        <loader/>

        <collection id="linesDc" property="lines"/>
    </instance>

    <collection id="customersDc" class="com.company.sales.entity.Customer" view="_minimal">
        <loader>
            <query><![CDATA[select e from sales_Customer e]]></query>
        </loader>
    </collection>
</data>

In this case, the following data components are created:

  • DataContext instance.

  • InstanceContainer with orderDc id and InstanceLoader for the Order entity.

  • CollectionPropertyContainer with linesDc id for the OrderLines entity. It is bound to the Order.lines collection attribute.

  • CollectionContainer with customersDc id for the Customer entity. It is loaded by CollectionLoader using the specified query.

The data containers can be used in visual components as follows:

<layout>
    <dateField dataContainer="orderDc" property="date"/> (1)
    <form id="form" dataContainer="orderDc"> (2)
        <column>
            <textField property="amount"/>
            <lookupPickerField id="customerField" property="customer"
                               optionsContainer="customersDc"/> (3)
        </column>
    </form>
    <table dataContainer="linesDc"> (4)
        <columns>
            <column id="product"/>
            <column id="quantity"/>
        </columns>
    </table>
1 Standalone fields have dataContainer and property attributes.
2 form propagates dataContainer to its fields so they need only property attribute.
3 Lookup fields have optionsContainer attribute.
4 Tables have only dataContainer attribute.
3.5.3.4.2. Programmatic Creation of Data Components

Data components can be created and used in visual components programmatically.

In the example below, we create an editor screen with the same data and visual components as defined in the previous section using only Java code without any XML descriptor.

package com.company.sales.web.order;

import com.company.sales.entity.Customer;
import com.company.sales.entity.Order;
import com.company.sales.entity.OrderLine;
import com.haulmont.cuba.core.global.View;
import com.haulmont.cuba.gui.UiComponents;
import com.haulmont.cuba.gui.components.*;
import com.haulmont.cuba.gui.components.data.options.ContainerOptions;
import com.haulmont.cuba.gui.components.data.table.ContainerTableItems;
import com.haulmont.cuba.gui.components.data.value.ContainerValueSource;
import com.haulmont.cuba.gui.model.*;
import com.haulmont.cuba.gui.screen.PrimaryEditorScreen;
import com.haulmont.cuba.gui.screen.StandardEditor;
import com.haulmont.cuba.gui.screen.Subscribe;
import com.haulmont.cuba.gui.screen.UiController;

import javax.inject.Inject;
import java.sql.Date;

@UiController("sales_Order.edit")
public class OrderEdit extends StandardEditor<Order> {

    @Inject
    private DataComponents dataComponents; (1)
    @Inject
    private UiComponents uiComponents;

    private InstanceContainer<Order> orderDc;
    private CollectionPropertyContainer<OrderLine> linesDc;
    private CollectionContainer<Customer> customersDc;
    private InstanceLoader<Order> orderDl;
    private CollectionLoader<Customer> customersDl;

    @Subscribe
    protected void onInit(InitEvent event) {
        createDataComponents();
        createUiComponents();
    }

    private void createDataComponents() {
        DataContext dataContext = dataComponents.createDataContext();
        getScreenData().setDataContext(dataContext); (2)

        orderDc = dataComponents.createInstanceContainer(Order.class);

        orderDl = dataComponents.createInstanceLoader();
        orderDl.setContainer(orderDc); (3)
        orderDl.setDataContext(dataContext); (4)
        orderDl.setView("order-edit");

        linesDc = dataComponents.createCollectionContainer(
                OrderLine.class, orderDc, "lines"); (5)

        customersDc = dataComponents.createCollectionContainer(Customer.class);

        customersDl = dataComponents.createCollectionLoader();
        customersDl.setContainer(customersDc);
        customersDl.setDataContext(dataContext);
        customersDl.setQuery("select e from sales_Customer e"); (6)
        customersDl.setView(View.MINIMAL);
    }

    private void createUiComponents() {
        DateField<Date> dateField = uiComponents.create(DateField.TYPE_DATE);
        getWindow().add(dateField);
        dateField.setValueSource(new ContainerValueSource<>(orderDc, "date")); (7)

        Form form = uiComponents.create(Form.class);
        getWindow().add(form);

        LookupPickerField<Customer> customerField = uiComponents.create(LookupField.of(Customer.class));
        form.add(customerField);
        customerField.setValueSource(new ContainerValueSource<>(orderDc, "customer"));
        customerField.setOptions(new ContainerOptions<>(customersDc)); (8)

        TextField<Integer> amountField = uiComponents.create(TextField.TYPE_INTEGER);
        amountField.setValueSource(new ContainerValueSource<>(orderDc, "amount"));

        Table<OrderLine> table = uiComponents.create(Table.of(OrderLine.class));
        getWindow().add(table);
        getWindow().expand(table);
        table.setItems(new ContainerTableItems<>(linesDc)); (9)

        Button okButton = uiComponents.create(Button.class);
        okButton.setAction(getWindow().getActionNN(WINDOW_COMMIT_AND_CLOSE));
        getWindow().add(okButton);

        Button cancelButton = uiComponents.create(Button.class);
        cancelButton.setAction(getWindow().getActionNN(WINDOW_CLOSE));
        getWindow().add(cancelButton);
    }

    @Override
    protected InstanceContainer<Order> getEditedEntityContainer() { (10)
        return orderDc;
    }

    @Subscribe
    protected void onBeforeShow(BeforeShowEvent event) { (11)
        orderDl.load();
        customersDl.load();
    }
}
1 DataComponents is a factory to create data components.
2 DataContext instance is registered in the screen for standard commit action to work properly.
3 orderDl loader will load data to orderDc container.
4 orderDl loader will merge loaded entities into data context for change tracking.
5 linesDc is created as a property container.
6 A query is specified for the customersDl loader.
7 ContainerValueSource is used to bind single fields to containers.
8 ContainerOptions is used to provide options to lookup fields.
9 ContainerTableItems is used to bind tables to containers.
10 getEditedEntityContainer() is overridden to specify the container instead of @EditedEntityContainer annotation.
11 Load data before showing the screen. The edited entity id will be set to orderDl by the framework automatically.
3.5.3.4.3. Dependencies Between Data Components

Sometimes you need to load and display data which depends on other data in the same screen. For example, on the screenshot below the left table displays the list of orders and the right one displays the list of lines of the selected order. The right list is refreshed each time the selected item in the left list changes.

dep data comp
Figure 26. Dependent Tables

In this example, the Order entity contains the orderLines attribute which is a one-to-many collection. So the simplest way to implement the screen is to load the list of orders with a view containing the orderLines attribute and use a property container to hold the list of dependent lines. Then bind the left table to the master container and the right table to the property container.

But this approach has the following performance implication: you will load lines for all orders from the left table, even though you display the order lines only for a single order at a time. And the longer the list of orders is, the more unneeded data is loaded, because there is a little chance that the user will go through all orders to see their lines. This is why we recommend using property containers and wide views only when loading a single master item, for example in an order editor screen.

Also, the master entity may have no direct property pointing to the dependent entity. In this case, the above approach with property container would not work at all.

The common approach to organize relations between data in a screen is to use queries with parameters. The dependent loader contains a query with a parameter which links data to the master, and when the current item in the master container changes, you set the parameter and trigger the dependent loader.

Below is an example of the screen which has two dependent container/loader pairs and the tables bound to them.

<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd">
    <data>
        <collection id="ordersDc" (1)
                    class="com.company.sales.entity.Order" view="order-with-customer">
            <loader id="ordersDl">
                <query>select e from sales_Order e></query>
            </loader>
        </collection>

        <collection id="orderLinesDc" (2)
                    class="com.company.sales.entity.OrderLine" view="_local">
            <loader id="orderLinesDl">
                <query>select e from sales_OrderLine e where e.order = :order</query>
            </loader>
        </collection>
    </data>
    <layout>
        <hbox id="mainBox" width="100%" height="100%" spacing="true">
            <table id="ordersTable" width="100%" height="100%"
                   dataContainer="ordersDc"> (3)
                <columns>
                    <column id="customer"/>
                    <column id="date"/>
                    <column id="amount"/>
                </columns>
                <rows/>
            </table>
            <table id="orderLinesTable" width="100%" height="100%"
                   dataContainer="orderLinesDc"> (4)
                <columns>
                    <column id="product"/>
                    <column id="quantity"/>
                </columns>
                <rows/>
            </table>
        </hbox>
    </layout>
</window>
1 Master container and loader.
2 Dependent container and loader.
3 Master table.
4 Dependent table.
package com.company.sales.web.order;

import com.company.sales.entity.Order;
import com.company.sales.entity.OrderLine;
import com.haulmont.cuba.gui.model.CollectionLoader;
import com.haulmont.cuba.gui.model.InstanceContainer;
import com.haulmont.cuba.gui.screen.*;
import javax.inject.Inject;

@UiController("order-list")
@UiDescriptor("order-list.xml")
@LookupComponent("ordersTable")
public class OrderList extends StandardLookup<Order> { (1)

    @Inject
    private CollectionLoader<Order> ordersDl;
    @Inject
    private CollectionLoader<OrderLine> orderLinesDl;

    @Subscribe
    protected void onBeforeShow(BeforeShowEvent event) {
        ordersDl.load(); (2)
    }

    @Subscribe(id = "ordersDc", target = Target.DATA_CONTAINER)
    protected void onOrdersDcItemChange(InstanceContainer.ItemChangeEvent<Order> event) {
        orderLinesDl.setParameter("order", event.getItem()); (3)
        orderLinesDl.load();
    }
}
1 The screen controller class has no @LoadDataBeforeShow annotation, so the loaders will not be triggered automatically.
2 The master loader is triggered in the BeforeShowEvent handler.
3 In the ItemChangeEvent handler of the master container, a parameter is set to the dependent loader and it is triggered.

The DataLoadCoordinator facet allows you to link data components declaratively without writing any Java code.

3.5.3.4.4. Using Screen Parameters in Loaders

It is often required to load data in a screen depending on parameters passed to that screen. Below is an example of a browse screen which accepts a parameter and uses it to filter the loaded data.

Suppose we have two entities: Country and City. The City entity has country attribute which is a reference to Country. The cities browser accepts a country instance and shows cities only of this country.

First, consider the cities screen XML descriptor. It’s loader contains a query with a parameter:

<collection id="citiesDc"
            class="com.company.demo.entity.City"
            view="_local">
    <loader id="citiesDl">
        <query>
            <![CDATA[select e from demo_City e where e.country = :country]]>
        </query>
    </loader>
</collection>

The cities screen controller contains a public setter for the parameter and uses the parameter in BeforeShowEvent handler. Notice that the screen has no @LoadDataBeforeShow annotation, because loading is triggered explicitly:

@UiController("demo_City.browse")
@UiDescriptor("city-browse.xml")
@LookupComponent("citiesTable")
public class CityBrowse extends StandardLookup<City> {

    @Inject
    private CollectionLoader<City> citiesDl;

    private Country country;

    public void setCountry(Country country) {
        this.country = country;
    }

    @Subscribe
    private void onBeforeShow(BeforeShowEvent event) {
        if (country == null)
            throw new IllegalStateException("country parameter is null");
        citiesDl.setParameter("country", country);
        citiesDl.load();
    }
}

The cities screen can be opened from another screen passing a country as follows:

@Inject
private ScreenBuilders screenBuilders;

private void showCitiesOfCountry(Country country) {
    CityBrowse cityBrowse = screenBuilders.screen(this)
            .withScreenClass(CityBrowse.class)
            .build();
    cityBrowse.setCountry(country);
    cityBrowse.show();
}
3.5.3.4.5. Custom Sorting

Sorting of UI tables by entity attributes is performed by CollectionContainerSorter which is set for a CollectionContainer. The standard implementation sorts data in memory if it fits in one page of loaded data, otherwise it sends a new request to the database with the appropriate "order by" clause. The "order by" clause is created by the JpqlSortExpressionProvider bean on the middle tier.

Some entity attributes may require a special implementation of sorting. Below we explain how to customize sorting on a simple example: suppose there is a Foo entity with a number attribute of type String, but we know that the attribute actually stores only numeric values. So we want the sort order to be 1, 2, 3, 10, 11. With the default behavior, the order would be 1, 10, 11, 2, 3.

First, create a subclass of the CollectionContainerSorter class in the web module for sorting in memory:

package com.company.demo.web;

import com.company.demo.entity.Foo;
import com.haulmont.chile.core.model.MetaClass;
import com.haulmont.chile.core.model.MetaPropertyPath;
import com.haulmont.cuba.core.entity.Entity;
import com.haulmont.cuba.core.global.Sort;
import com.haulmont.cuba.gui.model.BaseCollectionLoader;
import com.haulmont.cuba.gui.model.CollectionContainer;
import com.haulmont.cuba.gui.model.impl.CollectionContainerSorter;
import com.haulmont.cuba.gui.model.impl.EntityValuesComparator;

import javax.annotation.Nullable;
import java.util.Comparator;
import java.util.Objects;

public class CustomCollectionContainerSorter extends CollectionContainerSorter {

    public CustomCollectionContainerSorter(CollectionContainer container,
                                           @Nullable BaseCollectionLoader loader) {
        super(container, loader);
    }

    @Override
    protected Comparator<? extends Entity> createComparator(Sort sort, MetaClass metaClass) {
        MetaPropertyPath metaPropertyPath = Objects.requireNonNull(
                metaClass.getPropertyPath(sort.getOrders().get(0).getProperty()));

        if (metaPropertyPath.getMetaClass().getJavaClass().equals(Foo.class)
                && "number".equals(metaPropertyPath.toPathString())) {
            boolean isAsc = sort.getOrders().get(0).getDirection() == Sort.Direction.ASC;
            return Comparator.comparing(
                    (Foo e) -> e.getNumber() == null ? null : Integer.valueOf(e.getNumber()),
                    EntityValuesComparator.asc(isAsc));
        }
        return super.createComparator(sort, metaClass);
    }
}

If you need the customized sorting just in a few screens, you can instantiate CustomCollectionContainerSorter right in the screen:

public class FooBrowse extends StandardLookup<Foo> {

    @Inject
    private CollectionContainer<Foo> fooDc;
    @Inject
    private CollectionLoader<Foo> fooDl;

    @Subscribe
    private void onInit(InitEvent event) {
        CustomCollectionContainerSorter sorter = new CustomCollectionContainerSorter(fooDc, fooDl);
        fooDc.setSorter(sorter);
    }
}

If your sorter defines some global behavior, create your own factory which instantiates sorters system-wide:

package com.company.demo.web;

import com.haulmont.cuba.gui.model.*;
import javax.annotation.Nullable;

public class CustomSorterFactory extends SorterFactory {

    @Override
    public Sorter createCollectionContainerSorter(CollectionContainer container,
                                                  @Nullable BaseCollectionLoader loader) {
        return new CustomCollectionContainerSorter(container, loader);
    }
}

Register the factory in web-spring.xml to override the default factory:

<bean id="cuba_SorterFactory" class="com.company.demo.web.CustomSorterFactory"/>

Now create own implementation of JpqlSortExpressionProvider in the core module for sorting at the database level:

package com.company.demo.core;

import com.company.demo.entity.Foo;
import com.haulmont.chile.core.model.MetaPropertyPath;
import com.haulmont.cuba.core.app.DefaultJpqlSortExpressionProvider;

public class CustomSortExpressionProvider extends DefaultJpqlSortExpressionProvider {

    @Override
    public String getDatatypeSortExpression(MetaPropertyPath metaPropertyPath, boolean sortDirectionAsc) {
        if (metaPropertyPath.getMetaClass().getJavaClass().equals(Foo.class)
                && "number".equals(metaPropertyPath.toPathString())) {
            return String.format("CAST({E}.%s BIGINT)", metaPropertyPath.toString());
        }
        return String.format("{E}.%s", metaPropertyPath.toString());
    }
}

Register the expression provider in spring.xml to override the default one:

<bean id="cuba_JpqlSortExpressionProvider" class="com.company.demo.core.CustomSortExpressionProvider"/>

3.5.4. Non-Visual Components

A screen can contain non-visual components defined in the facets element of the XML descriptor. The framework provides the following non-visual components:

The application or an add-on can provide its own non-visual components. In order to create a custom facet, follow the steps below:

  1. Create an interface extending com.haulmont.cuba.gui.components.Facet.

  2. Create implementation class based on com.haulmont.cuba.web.gui.WebAbstractFacet.

  3. Create a Spring bean implementing the com.haulmont.cuba.gui.xml.FacetProvider interface parameterized by the type of your facet.

  4. Create an XSD to be used in screens XML.

Classes ClipboardTrigger, WebClipboardTrigger and ClipboardTriggerFacetProvider of the framework can be a good example of creating a facet.

3.5.4.1. Timer

Timer is a non-visual component designed to run some screen controller code at specified time intervals. The timer works in a thread that handles user interface events, therefore it can update screen components. Timer stops working when a screen it was created for is closed.

The main approach for creating timers is by declaring them in the facets element of the screen XML descriptor.

Timers are described using the timer element.

  • delay is a required attribute; it defines timer interval in milliseconds.

  • autostart – an optional attribute; when it is set to true, timer starts immediately after the screen is opened. By default, the value is false, which means that the timer will start only when its start() method is invoked.

  • repeating – an optional attribute, turns on repeated executions of the timer. If the attribute is set to true, the timer runs in cycles at equal intervals defined in the delay attribute. Otherwise, the timer runs only once – delay milliseconds after the timer start.

To execute some code on timer, subscribe to its TimerActionEvent in the screen controller.

An example of defining a timer and subscribing to it in the controller:

<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd" ...>
    <facets>
        <timer id="myTimer" delay="3000" autostart="true" repeating="true"/>
    </facets>
@Inject
private Notifications notifications;

@Subscribe("myTimer")
private void onTimer(Timer.TimerActionEvent event) {
    notifications.create(Notifications.NotificationType.TRAY)
        .withCaption("on timer")
        .show();
}

A timer can be injected into a controller field, or acquired using the getWindow().getFacet() method. Timer execution can be controlled using the timer’s start() and stop() methods. For an already active timer, start() invocation will be ignored. After stopping the timer using stop() method, it can be started again with start().

Example of defining a timer in an XML descriptor and using timer listeners in a controller:

<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd" ...>
    <facets>
        <timer id="helloTimer" delay="5000"/>
    <facets>
@Inject
private Timer helloTimer;
@Inject
private Notifications notifications;

@Subscribe("helloTimer")
protected void onHelloTimerTimerAction(Timer.TimerActionEvent event) { (1)
    notifications.create()
            .withCaption("Hello")
            .show();
}

@Subscribe("helloTimer")
protected void onHelloTimerTimerStop(Timer.TimerStopEvent event) { (2)
    notifications.create()
            .withCaption("Timer is stopped")
            .show();
}

@Subscribe
protected void onInit(InitEvent event) { (3)
    helloTimer.start();
}
1 timer execution handler
2 timer stop event
3 start the timer

A timer can be created in a controller, in this case it should be added to the screen implicitly using the addFacet() method, for example:

@Inject
private Notifications notifications;
@Inject
private Facets facets;

@Subscribe
protected void onInit(InitEvent event) {
    Timer helloTimer = facets.create(Timer.class);
    getWindow().addFacet(helloTimer); (1)
    helloTimer.setId("helloTimer"); (2)
    helloTimer.setDelay(5000);
    helloTimer.setRepeating(true);

    helloTimer.addTimerActionListener(e -> { (3)
        notifications.create()
                .withCaption("Hello")
                .show();
    });

    helloTimer.addTimerStopListener(e -> { (4)
        notifications.create()
                .withCaption("Timer is stopped")
                .show();
    });

    helloTimer.start(); (5)
}
1 add timer to the screen
2 set timer parameters
3 add execution handler
4 add stop listener
5 start the timer
3.5.4.2. ClipboardTrigger

ClipboardTrigger is a non-visual component which allows a user to copy text from a field to the clipboard. It is defined in the facets element of the screen XML descriptor and has the following attributes:

  • input - identifier of a text field, must be a subclass of TextInputField like TextField, TextArea and so on.

  • button - identifier of a Button which triggers the copying.

For example:

<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd" ...>
    <facets>
        <clipboardTrigger id="clipper" button="clipBtn" input="textArea"/>
    </facets>
    <layout expand="textArea" spacing="true">
        <textArea id="textArea" width="100%"/>
        <button id="clipBtn" caption="Clip text"/>
    </layout>
</window>
@Inject
private Notifications notifications;

@Subscribe("clipBtn")
private void onClipBtnClick(Button.ClickEvent event) {
    notifications.create().withCaption("Copied to clipboard").show();
}
3.5.4.3. DataLoadCoordinator

DataLoadCoordinator facet is designed for declarative linking of data loaders to data containers, visual components and screen events. It can work in two modes:

  • In automatic mode, it relies on parameter names with special prefixes. The prefix denotes a component which produces the parameter value and change events. If the loader has no parameters in its query text (although it can have parameters in query conditions), it is refreshed on BeforeShowEvent in Screen or on AttachEvent in ScreenFragment.

    By default, the parameter prefix is container_ for data containers and component_ for visual components.

  • In manual mode, the links are specified in the facet markup or via its API.

Semi-automatic mode is also possible, when some links are specified explicitly and the rest is configured automatically.

When using DataLoadCoordinator in a screen, the @LoadDataBeforeShow annotation on the screen controller has no effect: the loading of data is controlled by the facet and by custom event handlers, if any.

See usage examples below.

  1. Automatic configuration. The auto attribute is set to true.

    <window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd"
            xmlns:c="http://schemas.haulmont.com/cuba/screen/jpql_condition.xsd" ...>
        <data readOnly="true">
            <collection id="ownersDc" class="com.company.demo.entity.Owner" view="owner-view">
                <loader id="ownersDl">
                    <query>
                        <![CDATA[select e from demo_Owner e]]> (1)
                        <condition>
                            <c:jpql>
                                <c:where>e.category = :component_categoryFilterField</c:where> (2)
                            </c:jpql>
                            <c:jpql>
                                <c:where>e.name like :component_nameFilterField</c:where> (3)
                            </c:jpql>
                        </condition>
                    </query>
                </loader>
            </collection>
            <collection id="petsDc" class="com.company.demo.entity.Pet">
                <loader id="petsDl">
                    <query><![CDATA[select e from demo_Pet e where e.owner = :container_ownersDc]]></query> (4)
                </loader>
            </collection>
        </data>
        <facets>
            <dataLoadCoordinator auto="true"/>
        </facets>
        <layout>
            <pickerField id="categoryFilterField" metaClass="demo_OwnerCategory"/>
            <textField id="nameFilterField"/>
    1 - there are no parameters in the query, so the ownersDl loader will be triggered on BeforeShowEvent.
    2 - the ownersDl loader will also be triggered on categoryFilterField component value change.
    3 - the ownersDl loader will also be triggered on nameFilterField component value change. As the condition uses the like clause, the value will be automatically wrapped in '(?i)% %' to provide the case-insensitive search.
    4 - the petsDl is triggered on the ownersDc data container item change.
  2. Manual configuration. The auto attribute is absent (or set to false), the nested entries define when the data loaders must be triggered.

    <window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd"
            xmlns:c="http://schemas.haulmont.com/cuba/screen/jpql_condition.xsd" ...>
        <data readOnly="true">
            <collection id="ownersDc" class="com.company.demo.entity.Owner" view="owner-view">
                <loader id="ownersDl">
                    <query>
                        <![CDATA[select e from demo_Owner e]]>
                        <condition>
                            <c:jpql>
                                <c:where>e.category = :category</c:where>
                            </c:jpql>
                            <c:jpql>
                                <c:where>e.name like :name</c:where>
                            </c:jpql>
                        </condition>
                    </query>
                </loader>
            </collection>
            <collection id="petsDc" class="com.company.demo.entity.Pet">
                <loader id="petsDl">
                    <query><![CDATA[select e from demo_Pet e where e.owner = :owner]]></query>
                </loader>
            </collection>
        </data>
        <facets>
            <dataLoadCoordinator>
                <refresh loader="ownersDl"
                         onScreenEvent="Init"/> (1)
    
                <refresh loader="ownersDl" param="category"
                         onComponentValueChanged="categoryFilterField"/> (2)
    
                <refresh loader="ownersDl" param="name"
                         onComponentValueChanged="nameFilterField" likeClause="CASE_INSENSITIVE"/> (3)
    
                <refresh loader="petsDl" param="owner"
                         onContainerItemChanged="ownersDc"/> (4)
            </dataLoadCoordinator>
        </facets>
        <layout>
            <pickerField id="categoryFilterField" metaClass="demo_OwnerCategory"/>
            <textField id="nameFilterField"/>
    1 - the ownersDl loader will be triggered on InitEvent.
    2 - the ownersDl loader will also be triggered on categoryFilterField component value change.
    3 - the ownersDl loader will also be triggered on nameFilterField component value change. The likeClause attribute causes the value to be wrapped in '(?i)% %' to provide the case-insensitive search.
    4 - the petsDl is triggered on the ownersDc data container item change.
  3. Semi-automatic configuration. When the auto attribute is set to true and there are some manually configured triggers, the facet will configure automatically all loaders that have no manual configuration.

    <window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd" ...>
        <data readOnly="true">
            <collection id="ownersDc" class="com.company.demo.entity.Owner" view="owner-view">
                <loader id="ownersDl">
                    <query>
                        <![CDATA[select e from demo_Owner e]]>
                    </query>
                </loader>
            </collection>
            <collection id="petsDc" class="com.company.demo.entity.Pet">
                <loader id="petsDl">
                    <query><![CDATA[select e from demo_Pet e where e.owner = :container_ownersDc]]></query> (1)
                </loader>
            </collection>
        </data>
        <facets>
            <dataLoadCoordinator auto="true">
                <refresh loader="ownersDl" onScreenEvent="Init"/> (2)
            </dataLoadCoordinator>
        </facets>
    1 - the petsDl is configured automatically and triggered on the ownersDc data container item change.
    2 - the ownersDl loader is configured manually and will be triggered on InitEvent.

3.5.5. Actions. The Action Interface

Action is an interface that abstracts an action (in other words, some function) from a visual component. It is particularly useful when the same action can be invoked from different visual components (for example, from button and table context menu). In addition, this interface allows you to provide the action with additional properties, such as name, flags of accessibility and visibility, etc.

Below are the Action interface methods:

  • actionPerform() is invoked by a visual component associated with this action. An instance of the caller is passed to the method.

  • getId() returns an identifier of the action. The identifier is usually set by a constructor of a class that implements Action and does not change throughout the lifecycle of the created action object.

  • Methods for getting and setting caption, description, shortcut, icon, enabled, visible properties. Typically, all these properties are used by related visual components to set their own corresponding properties.

  • addPropertyChangeListener(), removePropertyChangeListener() methods used to add and remove listeners which handle changes to the abovementioned properties. A listener receives notification of java.beans.PropertyChangeEvent type, which contains the name of the changed property, its old and new values.

  • refreshState() - a method that can be implemented in a particular action class to initialize the abovementioned properties in accordance to some external factors, such as user rights. It is usually invoked in constructors of implementing classes or from related visual components.

  • addOwner(), removeOwner(), getOwner(), getOwners() – methods used to control relation between the action and visual components.

It is recommended to implement actions using the declarative creation or by inheriting from the BaseAction class. Furthermore, there is a set of standard actions applicable for tables and picker components.

Visual components associated with an action can be of two types:

  • Visual components with a single action implement the Component.ActionOwner interface. These are Button and LinkButton.

    An action is linked to the component by invoking the ActionOwner.setAction() method of the component. At this point, the component replaces its properties with corresponding properties of the action (see components overview for details).

  • Visual components containing several actions implement the Component.ActionsHolder interface. These are Window, Fragment, DataGrid, Table and its inheritors, Tree, PopupButton, PickerField, LookupPickerField.

    The ActionsHolder.addAction() method is used to add actions to the component. Implementation of this method in the component checks whether it already contains an action with the same identifier. If yes, then the existing action will be replaced with the new one. Therefore, it is possible, for example, to declare a standard action in a screen descriptor and then create a new one in the controller with different properties and add it to the component.

3.5.5.1. Declarative Creation of Actions

You can specify a set of actions in an XML screen descriptor for any component that implements the Component.ActionsHolder interface, including the entire window or fragment. This is done in the actions element, which contains nested action elements.

The action element can have the following attributes:

  • id − identifier, which should be unique within the ActionsHolder component.

  • caption – action name.

  • description – action description.

  • enable – accessibility flag (true / false).

  • icon – action icon.

  • primary - attribute that indicates if a button representing this action should be highlighted with a special visual style (true / false).

    The highlighting is available by default in the hover theme; to enable this feature in the halo theme, set true for the $cuba-highlight-primary-action style variable.

    The create standard list action and the lookupSelectAction in the lookup screen are primary by default.

    actions primary
  • shortcut - a keyboard shortcut.

    Shortcut values can be hard-coded in the XML descriptor. Possible modifiers, ALT, CTRL, SHIFT, are separated by the "-" character. For example:

    <action id="create" shortcut="ALT-N"/>

    To avoid the hard-coded values, you can use the predefined shortcut aliases from the list below, for example:

    <action id="edit" shortcut="${TABLE_EDIT_SHORTCUT}"/>
    • TABLE_EDIT_SHORTCUT

    • COMMIT_SHORTCUT

    • CLOSE_SHORTCUT

    • FILTER_APPLY_SHORTCUT

    • FILTER_SELECT_SHORTCUT

    • NEXT_TAB_SHORTCUT

    • PREVIOUS_TAB_SHORTCUT

    • PICKER_LOOKUP_SHORTCUT

    • PICKER_OPEN_SHORTCUT

    • PICKER_CLEAR_SHORTCUT

    Another option is to use the fully qualified name of the Config interface and method which returns shortcut:

    <action id="remove" shortcut="${com.haulmont.cuba.client.ClientConfig#getTableRemoveShortcut}"/>
  • visible – visibility flag (true / false).

The examples of action declaration and handling are provided below.

  • Declaring actions for the whole screen:

    <window>
        <actions>
            <action id="sayHello" caption="msg://sayHello" shortcut="ALT-T"/>
        </actions>
    
        <layout>
            <button action="sayHello"/>
        </layout>
    </window>
    // controller
    @Inject
    private Notifications notifications;
    
    @Subscribe("sayHello")
    protected void onSayHelloActionPerformed(Action.ActionPerformedEvent event) {
        notifications.create()
                    .withCaption("Hello")
                    .withType(Notifications.NotificationType.HUMANIZED)
                    .show();
    }

    In the example above, an action with sayHello identifier and a caption from the screen’s message pack is declared. This action is bound to a button, which caption will be set to the action’s name. The screen controller subscribes to the action’s ActionPerformedEvent, so the onSayHelloActionPerformed() method will be invoked when the user clicks the button or presses the ALT-T keyboard shortcut.

  • Declaring actions for PopupButton:

    <popupButton id="sayBtn" caption="Say">
        <actions>
            <action id="hello" caption="Say Hello"/>
            <action id="goodbye" caption="Say Goodbye"/>
        </actions>
    </popupButton>
    // controller
    @Inject
    private Notifications notifications;
    
    private void showNotification(String message) {
        notifications.create()
                .withCaption(message)
                .withType(NotificationType.HUMANIZED)
                .show();
    }
    
    @Subscribe("sayBtn.hello")
    private void onSayBtnHelloActionPerformed(Action.ActionPerformedEvent event) {
        notifications.create()
                .withCaption("Hello")
                .show();
    }
    
    @Subscribe("sayBtn.goodbye")
    private void onSayBtnGoodbyeActionPerformed(Action.ActionPerformedEvent event) {
        notifications.create()
                .withCaption("Hello")
                .show();
    }
  • Declaring actions for Table:

    <groupTable id="customersTable" width="100%" dataContainer="customersDc">
        <actions>
            <action id="create" type="create"/>
            <action id="edit" type="edit"/>
            <action id="remove" type="remove"/>
            <action id="copy" caption="Copy" icon="COPY" trackSelection="true"/>
        </actions>
        <columns>
            <!-- -->
        </columns>
        <rowsCount/>
        <buttonsPanel alwaysVisible="true">
            <!-- -->
            <button action="customersTable.copy"/>
        </buttonsPanel>
    </groupTable>
    // controller
    
    @Subscribe("customersTable.copy")
    protected void onCustomersTableCopyActionPerformed(Action.ActionPerformedEvent event) {
        // ...
    }

    In this example, the copy action is declared in addition to create, edit and remove standard actions of the table. The trackSelection="true" attribute means that the action and corresponding button become disabled if no row is selected in the table. It is useful if the action is intended to be executed for a currently selected table row.

  • Declaring PickerField actions:

    <pickerField id="userPickerField" dataContainer="customerDc" property="user">
        <actions>
            <action id="lookup" type="picker_lookup"/>
            <action id="show" description="Show user" icon="USER"/>
        </actions>
    </pickerField>
    // controller
    
    @Subscribe("userPickerField.show")
    protected void onUserPickerFieldShowActionPerformed(Action.ActionPerformedEvent event) {
        //
    }

    In the example above, the standard picker_lookup action and an additional show action are declared for the PickerField component. Since PickerField buttons that display actions use icons instead of captions, the caption attribute is not set. The description attribute allows you to display a tooltip when hovering over the action button.

You can obtain a reference to any declared action in the screen controller either directly by injection, or from a component that implements the Component.ActionsHolder interface. It can be useful to set action properties programmatically. For example:

@Named("customersTable.copy")
private Action customersTableCopy;

@Inject
private PickerField<User> userPickerField;

@Subscribe
protected void onBeforeShow(BeforeShowEvent event) {
    customersTableCopy.setEnabled(false);
    userPickerField.getActionNN("show").setEnabled(false);
}
3.5.5.2. Standard Actions

Standard actions are provided by the framework to solve common tasks, such as invocation of an edit screen for an entity selected in a table. A standard action can be declared in screen XML descriptor by specifying its type in the type attribute, for example:

<!-- in a table -->
<action type="create"/>

<!-- in a PickerField -->
<action id="lookup" type="picker_lookup"/>

The standard action configures itself depending on its type and owning component. It may or may not require additional parameters.

There are two categories of standard actions:

You can create similar actions or override existing standard types in your project.

For example, imagine that you need an action that would show the instance name of the currently selected entity in a table, and you would like to use this action in multiple screens by specifying its type only. Below are the steps to create such action.

  1. Create an action class and add the @ActionType annotation with the desired type name:

    package com.company.sample.web.actions;
    
    import com.haulmont.cuba.core.entity.Entity;
    import com.haulmont.cuba.core.global.MetadataTools;
    import com.haulmont.cuba.gui.ComponentsHelper;
    import com.haulmont.cuba.gui.Notifications;
    import com.haulmont.cuba.gui.components.ActionType;
    import com.haulmont.cuba.gui.components.Component;
    import com.haulmont.cuba.gui.components.actions.ItemTrackingAction;
    
    import javax.inject.Inject;
    
    @ActionType("showSelected")
    public class ShowSelectedAction extends ItemTrackingAction {
    
        @Inject
        private MetadataTools metadataTools;
    
        public ShowSelectedAction(String id) {
            super(id);
            setCaption("Show Selected");
        }
    
        @Override
        public void actionPerform(Component component) {
            Entity selected = getTarget().getSingleSelected();
            if (selected != null) {
                Notifications notifications = ComponentsHelper.getScreenContext(target).getNotifications();
                notifications.create()
                        .setType(Notifications.NotificationType.TRAY)
                        .setCaption(metadataTools.getInstanceName(selected))
                        .show();
            }
        }
    }
  2. In the web-spring.xml file, add <gui:actions> element with the base-packages attribute pointing to a package where to find your annotated actions:

    <beans ... xmlns:gui="http://schemas.haulmont.com/cuba/spring/cuba-gui.xsd">
        <!-- ... -->
        <gui:actions base-packages="com.company.sample.web.actions"/>
    </beans>
  3. Now you can use the action in screen descriptors by specifying its type:

    <groupTable id="customersTable">
        <actions>
            <action id="show" type="showSelected"/>
        </actions>
        <columns>
            <!-- ... -->
        </columns>
        <buttonsPanel>
            <button action="customersTable.show"/>
        </buttonsPanel>
    </groupTable>
3.5.5.2.1. List Component Actions

The framework provides a set of standard actions for visual components implementing the ListComponent interface (DataGrid, Table, GroupTable, TreeTable and Tree). They are located in the com.haulmont.cuba.gui.actions.list package.

An example of using standard actions in a table:

<groupTable id="customersTable" width="100%" dataContainer="customersDc">
    <actions>
        <action id="create" type="create"/>
        <action id="edit" type="edit"/>
        <action id="remove" type="remove"/>
    </actions>
    <columns>
        <column id="name"/>
        <column id="email"/>
    </columns>
    <buttonsPanel>
        <button id="createBtn" action="customersTable.create"/>
        <button id="editBtn" action="customersTable.edit"/>
        <button id="removeBtn" action="customersTable.remove"/>
    </buttonsPanel>
</groupTable>

The standard list component actions include the following types:

  • create type is implemented by the com.haulmont.cuba.gui.actions.list.CreateAction class. It is designed to create a new entity using its default edit screen.

  • edit type is implemented by the com.haulmont.cuba.gui.actions.list.EditAction class. It is designed to edit the selected entity using its default edit screen.

  • remove type is implemented by the com.haulmont.cuba.gui.actions.list.RemoveAction class. It is designed to remove the selected entity.

  • add type is implemented by the com.haulmont.cuba.gui.actions.list.AddAction class. It is designed to select an entity from a default lookup screen and add it to the associated data container. A typical use case for this action is to add entities to a many-to-many collection.

  • exclude type is implemented by the com.haulmont.cuba.gui.actions.list.ExcludeAction class. It is designed to remove an entity from a collection data container without removing it from the database. A typical use case for this action is to remove entities from a many-to-many collection.

  • refresh type is implemented by the com.haulmont.cuba.gui.actions.list.RefreshAction class. It is designed to reload the data container which is used by the list component.

  • excel type is implemented by the com.haulmont.cuba.gui.actions.list.ExcelAction class. It is designed to output the list component content to an XLS file.

  • bulkEdit type is implemented by the com.haulmont.cuba.gui.actions.list.BulkEditAction class. It is designed to change attribute values for several entity instances at once. The usage is the same as for any other actions, for instance:

    <table id="table" width="100%" dataContainer="customersCt">
        <actions>
            ...
            <action id="bulkEdit" type="bulkEdit"/>
        </actions>
        ...
        <rowsCount/>
        <buttonsPanel alwaysVisible="true">
            ...
            <button action="table.bulkEdit"/>
        </buttonsPanel>
        <rows/>
    </table>

    In addition to that, the BulkEditorWindow can be created programmatically using the new com.haulmont.cuba.gui.BulkEditors bean:

    bulkEditors.builder(metaClass, table.getSelected(), getWindow().getFrameOwner())
               .withListComponent(table)
               .create()
               .show();

Standard actions provide default values for basic parameters: caption, icon and shortcut; and default behavior when executed. You can provide your own values to basic parameters in XML, just like for any other action. For example, you can specify a custom icon:

<action id="create" type="create" icon="USER"/>

In order to customize the execution behavior, you should subscribe to the action’s ActionPerformedEvent. All standard actions do not execute their code if an alternative action listener is provided. It means that your ActionPerformedEvent handler effectively overrides the default action behavior.

For example, the following code overrides the default create action behavior to create Customer entity using a specific screen opened as a modal dialog:

public class CustomerBrowse extends StandardLookup<Customer> {
    @Inject
    private GroupTable<Customer> customersTable;
    @Inject
    private ScreenBuilders screenBuilders;

    @Subscribe("customersTable.create")
    protected void onCustomersTableCreateActionPerformed(Action.ActionPerformedEvent event) {
        screenBuilders.editor(customersTable)
                .newEntity()
                .withScreenClass(CustomerEdit.class)     // specific editor screen
                .withLaunchMode(OpenMode.DIALOG)        // open as modal dialog
                .build()
                .show();
    }
}

You can customize behavior of the edit action in the same way as for create. See ScreenBuilders bean description for more details.

The add action also uses the ScreenBuilders bean, so you can customize it as follows:

public class CustomerEdit extends StandardEditor<Customer> {
    @Inject
    private ScreenBuilders screenBuilders;
    @Inject
    private Table<Employee> accountableTable;

    @Subscribe("accountableTable.add")
    protected void onAccountableTableAddActionPerformed(Action.ActionPerformedEvent event) {
        screenBuilders.lookup(Employee.class, this)
                .withListComponent(accountableTable)
                .withScreenClass(EmployeeBrowse.class)   // specific editor screen
                .withLaunchMode(OpenMode.DIALOG)        // open as modal dialog
                .build()
                .show();
    }
}
3.5.5.2.2. Picker Field Actions

The framework provides a set of standard actions for PickerField, LookupPickerField and SearchPickerField components.

An example of using standard actions in a picker component:

<pickerField id="userPickerField" dataContainer="employeeDc" property="user">
    <actions>
        <action id="lookup" type="picker_lookup"/>
        <action id="open" type="picker_open"/>
        <action id="clear" type="picker_clear"/>
    </actions>
</pickerField>

The standard picker component actions include the following types:

  • picker_lookup type is implemented by the com.haulmont.cuba.gui.actions.picker.LookupAction class. It is designed to select an entity instance from a lookup screen and set it to the picker field.

  • picker_open type is implemented by the com.haulmont.cuba.gui.actions.picker.OpenAction class. It is designed to open an editor screen for the entity currently selected in the picker field.

  • picker_clear type is implemented by the com.haulmont.cuba.gui.actions.picker.ClearAction class. It is designed to clear the picker field.

Standard actions provide default values for basic parameters: caption, icon and shortcut; and default behavior when executed. You can provide your own values to basic parameters in XML, just like for any other action. For example, you can specify a custom icon:

<action id="open" type="picker_open" icon="USER"/>

In order to customize the execution behavior, you should subscribe to the action’s ActionPerformedEvent. All standard actions do not execute their code if an alternative action listener is provided. It means that your ActionPerformedEvent handler effectively overrides the default action behavior.

For example, the following code overrides the default picker_lookup action behavior to select User entity using a specific screen opened as a modal dialog:

public class CustomerBrowse extends StandardLookup<Customer> {
    @Inject
    private ScreenBuilders screenBuilders;
    @Inject
    private PickerField<User> userPickerField;

    @Subscribe("userPickerField.lookup")
    protected void onUserPickerFieldLookupActionPerformed(Action.ActionPerformedEvent event) {
        screenBuilders.lookup(User.class, this)
                .withField(userPickerField)
                .withScreenClass(UserBrowser.class) // specific lookup screen
                .withLaunchMode(OpenMode.DIALOG)    // open as modal dialog
                .build()
                .show();
    }

See ScreenBuilders bean description for more details.

3.5.5.3. BaseAction

BaseAction is a base class for actions implementation. It is recommended to derive custom actions from it when declarative actions creation functionality is insufficient.

When creating a custom action class, you should implement actionPerform() method and pass action identifier to the BaseAction constructor. You can override any property getters: getCaption(), getDescription(), getIcon(), getShortcut(), isEnabled(), isVisible(), isPrimary(). Standard implementations of these methods return values set by setter methods, except the getCaption() method. If the action name is not explicitly set by setCaption() method, it retrieves message using action identifier as key from the the localized message pack corresponding to the action class package. If there is no message with such key, then the key itself, i.e. the action identifier, is returned.

Alternatively, you can use the fluent API for setting properties and providing a lambda expression for handling the action: see withXYZ() methods.

BaseAction can change its enabled and visible properties depending on user permissions and current context.

BaseAction is visible if the following conditions are met:

  • setVisible(false) method was not called;

  • there is no hide UI permission for this action.

The action is enabled if the following conditions are met:

  • setEnabled(false) method was not called;

  • there are no hide or read-only UI permissions for this action;

  • isPermitted() method returns true;

  • isApplicable() method returns true.

Usage examples:

  • Button action:

    @Inject
    private Notifications notifications;
    @Inject
    private Button helloBtn;
    
    @Subscribe
    protected void onInit(InitEvent event) {
        helloBtn.setAction(new BaseAction("hello") {
            @Override
            public boolean isPrimary() {
                return true;
            }
    
            @Override
            public void actionPerform(Component component) {
                notifications.create()
                        .withCaption("Hello!")
                        .withType(Notifications.NotificationType.TRAY)
                        .show();
            }
        });
        // OR
        helloBtn.setAction(new BaseAction("hello")
                .withPrimary(true)
                .withHandler(e ->
                        notifications.create()
                                .withCaption("Hello!")
                                .withType(Notifications.NotificationType.TRAY)
                                .show()));
    }

    In this example, the helloBtn button caption will be set to the string located in the message pack with the hello key. You can override the getCaption() action method to initialize button name in a different way.

  • Action of a programmatically created PickerField:

    @Inject
    private UiComponents uiComponents;
    @Inject
    private Notifications notifications;
    @Inject
    private MessageBundle messageBundle;
    @Inject
    private HBoxLayout box;
    
    @Subscribe
    protected void onInit(InitEvent event) {
        PickerField pickerField = uiComponents.create(PickerField.NAME);
    
        pickerField.addAction(new BaseAction("hello") {
            @Override
            public String getCaption() {
                return null;
            }
    
            @Override
            public String getDescription() {
                return messageBundle.getMessage("helloDescription");
            }
    
            @Override
            public String getIcon() {
                return "icons/hello.png";
            }
    
            @Override
            public void actionPerform(Component component) {
                notifications.create()
                        .withCaption("Hello!")
                        .withType(Notifications.NotificationType.TRAY)
                        .show();
            }
        });
        // OR
        pickerField.addAction(new BaseAction("hello")
                .withCaption(null)
                .withDescription(messageBundle.getMessage("helloDescription"))
                .withIcon("icons/ok.png")
                .withHandler(e ->
                        notifications.create()
                                .withCaption("Hello!")
                                .withType(Notifications.NotificationType.TRAY)
                                .show()));
        box.add(pickerField);
    }

    In this example an anonymous BaseAction derived class is used to set the action of the picker field button. The button caption is not displayed, as an icon with a description, which pops up when hovering mouse cursor, is used instead.

  • Table action:

    @Inject
    private Notifications notifications;
    @Inject
    private Table<Customer> table;
    @Inject
    private Security security;
    
    @Subscribe
    protected void onInit(InitEvent event) {
        table.addAction(new HelloAction());
    }
    
    private class HelloAction extends BaseAction {
    
        public HelloAction() {
            super("hello");
        }
    
        @Override
        public void actionPerform(Component component) {
            notifications.create()
                    .withCaption("Hello " + table.getSingleSelected())
                    .withType(Notifications.NotificationType.TRAY)
                    .show();
        }
    
        @Override
        protected boolean isPermitted() {
            return security.isSpecificPermitted("myapp.allow-greeting");
        }
    
        @Override
        public boolean isApplicable() {
            return table != null && table.getSelected().size() == 1;
        }
    }

    In this example, the HelloAction class is declared, and its instance is added to the table’s actions list. The action is enabled for users who have myapp.allow-greeting security permission and only when a single table row is selected. The latter is possible because BaseAction’s target property is automatically assigned to the action when it is added to a ListComponent descendant (Table or Tree).

  • If you need an action, which becomes enabled when one or more table rows are selected, use BaseAction’s descendant - ItemTrackingAction, which adds default implementation of isApplicable() method:

    @Inject
    private Table table;
    @Inject
    private Notifications notifications;
    
    @Subscribe
    protected void onInit(InitEvent event) {
        table.addAction(new ItemTrackingAction("hello") {
            @Override
            public void actionPerform(Component component) {
                notifications.create()
                        .withCaption("Hello " + table.getSelected().iterator().next())
                        .withType(Notifications.NotificationType.TRAY)
                        .show();
            }
        });
    }

3.5.6. Dialogs

The Dialogs interface is designed to display standard dialogs windows. Its createMessageDialog(), createOptionDialog() and createInputDialog() methods are the entry points to the fluent API that allows you to construct and show dialogs.

Appearance of the dialogs can be customized using SCSS variables with $cuba-window-modal-* prefix. You can change these variables in the visual editor after creating a theme extension or a custom theme.

Message Dialog

In the following example, a message dialog is shown when the user clicks a button:

@Inject
private Dialogs dialogs;

@Subscribe("showDialogBtn")
protected void onShowDialogBtnClick(Button.ClickEvent event) {
    dialogs.createMessageDialog().withCaption("Information").withMessage("Message").show();
}

You can use \n characters for line breaks in messages. In order to show HTML, use the withContentMode() method with ContentMode.HTML parameter. When using HTML, don’t forget to escape data to prevent code injection.

The following methods allow you to customize the look and behavior of the message dialog:

  • withModal() - if false is passed, the dialog is shown as non-modal, which allows a user to interact with the other parts of the application.

  • withCloseOnClickOutside() - when true is passed and the dialog is modal, allows a user to close the dialog by clicking on the application window outside of the dialog.

  • withWidth(), withHeight() allow you to specify the desired dialog geometry.

For example:

@Inject
private Dialogs dialogs;

@Subscribe("showDialogBtn")
protected void onShowDialogBtnClick(Button.ClickEvent event) {
    dialogs.createMessageDialog()
            .withCaption("Information")
            .withMessage("<i>Message<i/>")
            .withContentMode(ContentMode.HTML)
            .withCloseOnClickOutside(true)
            .withWidth("100px")
            .withHeight("300px")
            .show();
}
Option Dialog

The option dialog displays a message and a set of buttons for user reaction. Use withActions() method to provide actions, each of which is represented by a button in the dialog. For example:

@Inject
private Dialogs dialogs;

@Subscribe("showDialogBtn")
protected void onShowDialogBtnClick(Button.ClickEvent event) {
    dialogs.createOptionDialog()
            .withCaption("Confirm")
            .withMessage("Are you sure?")
            .withActions(
                new DialogAction(DialogAction.Type.YES, Action.Status.PRIMARY).withHandler(e -> {
                    doSomething();
                }),
                new DialogAction(DialogAction.Type.NO)
            )
            .show();
}

When a button is clicked, the dialog closes and invokes actionPerform() method of the corresponding action.

The DialogAction base class is designed to create actions with standard names and icons. Five types of actions defined by the DialogAction.Type enum are supported: OK, CANCEL, YES, NO, CLOSE. Names of corresponding buttons are extracted from the main message pack.

The second parameter of the DialogAction constructor is used to assign a special visual style for a button representing the action. Status.PRIMARY highlights the corresponding button and makes it selected, which is provided by the c-primary-action style. If multiple actions with Status.PRIMARY are defined for the dialog, only the first action’s button will get the style and focus.

Input Dialog

Input dialog is a versatile tool which allows you to construct input forms using API and can often save you from creating screens for trivial data input. It enables entering values of different types, validates the input and provides different actions to be selected by the user.

Let’s consider some examples.

  1. Input dialog with parameters of standard types and standard OK/Cancel actions:

    @Inject
    private Dialogs dialogs;
    
    @Subscribe("showDialogBtn")
    private void onShowDialogBtnClick(Button.ClickEvent event) {
        dialogs.createInputDialog(this)
                .withCaption("Enter some values")
                .withParameters(
                        InputParameter.stringParameter("name")
                            .withCaption("Name").withRequired(true), (1)
                        InputParameter.doubleParameter("quantity")
                            .withCaption("Quantity").withDefaultValue(1.0), (2)
                        InputParameter.entityParameter("customer", Customer.class)
                            .withCaption("Customer"), (3)
                        InputParameter.enumParameter("status", Status.class)
                            .withCaption("Status") (4)
                )
                .withActions(DialogActions.OK_CANCEL) (5)
                .withCloseListener(closeEvent -> {
                    if (closeEvent.getCloseAction().equals(InputDialog.INPUT_DIALOG_OK_ACTION)) { (6)
                        String name = closeEvent.getValue("name"); (7)
                        Double quantity = closeEvent.getValue("quantity");
                        Customer customer = closeEvent.getValue("customer");
                        Status status = closeEvent.getValue("status");
                        // process entered values...
                    }
                })
                .show();
    }
    1 - specifies a mandatory string parameter.
    2 - specifies a double parameter with default value.
    3 - specifies an entity parameter.
    4 - specifies an enumeration parameter.
    5 - specifies a set of actions represented by buttons at the bottom of the dialog.
    6 - in the close listener, we can check what action was used by the user.
    7 - the close event contains entered values that can be obtained using parameter identifiers.
  2. Input dialog with a custom parameter:

    @Inject
    private Dialogs dialogs;
    @Inject
    private UiComponents uiComponents;
    
    @Subscribe("showDialogBtn")
    private void onShowDialogBtnClick(Button.ClickEvent event) {
        dialogs.createInputDialog(this)
                .withCaption("Enter some values")
                .withParameters(
                        InputParameter.stringParameter("name").withCaption("Name"),
                        InputParameter.parameter("customer") (1)
                                .withField(() -> {
                                    LookupField<Customer> field = uiComponents.create(
                                            LookupField.of(Customer.class));
                                    field.setOptionsList(dataManager.load(Customer.class).list());
                                    field.setCaption("Customer"); (2)
                                    field.setWidthFull();
                                    return field;
                                })
                )
                .withActions(DialogActions.OK_CANCEL)
                .withCloseListener(closeEvent -> {
                    if (closeEvent.getCloseAction().equals(InputDialog.INPUT_DIALOG_OK_ACTION)) {
                        String name = closeEvent.getValue("name");
                        Customer customer = closeEvent.getValue("customer"); (3)
                        // process entered values...
                    }
                })
                .show();
    }
    1 - specifies a custom parameter.
    2 - a caption for the custom parameter is specified in the created component.
    3 - value of the custom parameter is obtained in the same way as for standard parameters.
  3. Input dialog with custom actions:

    @Inject
    private Dialogs dialogs;
    
    @Subscribe("showDialogBtn")
    private void onShowDialogBtnClick(Button.ClickEvent event) {
        dialogs.createInputDialog(this)
                .withCaption("Enter some values")
                .withParameters(
                    InputParameter.stringParameter("name").withCaption("Name")
                )
                .withActions( (1)
                        InputDialogAction.action("confirm")
                                .withCaption("Confirm")
                                .withPrimary(true)
                                .withHandler(actionEvent -> {
                                    InputDialog dialog = actionEvent.getInputDialog();
                                    String name = dialog.getValue("name"); (2)
                                    dialog.closeWithDefaultAction(); (3)
                                    // process entered values...
                                }),
                        InputDialogAction.action("refuse")
                                .withCaption("Refuse")
                                .withValidationRequired(false)
                                .withHandler(actionEvent ->
                                    actionEvent.getInputDialog().closeWithDefaultAction())
                )
                .show();
    }
    1 - withActions() method can accept an array of custom actions.
    2 - in the action handler, you can get a parameter value from the dialog.
    3 - custom action does not close the dialog itself, so you should do it at some moment.
  4. Input dialog with custom validator:

    @Inject
    private Dialogs dialogs;
    
    @Subscribe("showDialogBtn")
    private void onShowDialogBtnClick(Button.ClickEvent event) {
        dialogs.createInputDialog(this)
                .withCaption("Enter some values")
                .withParameters(
                        InputParameter.stringParameter("name").withCaption("Name"),
                        InputParameter.entityParameter("customer", Customer.class).withCaption("Customer")
                )
                .withValidator(context -> { (1)
                    String name = context.getValue("name"); (2)
                    Customer customer = context.getValue("customer");
                    if (Strings.isNullOrEmpty(name) && customer == null) {
                        return ValidationErrors.of("Enter name or select a customer");
                    }
                    return ValidationErrors.none();
                })
                .withActions(DialogActions.OK_CANCEL)
                .withCloseListener(closeEvent -> {
                    if (closeEvent.getCloseAction().equals(InputDialog.INPUT_DIALOG_OK_ACTION)) {
                        String name = closeEvent.getValue("name");
                        Customer customer = closeEvent.getValue("customer");
                        // process entered values...
                    }
                })
                .show();
    }
    1 - the custom validator is needed to ensure at least one parameter is entered.
    2 - in the validator, parameter values can be obtained from the context object.

3.5.7. Notifications

Notifications are pop-ups displayed in the center or in the corner of the main application window. They can disappear automatically or when the user clicks on the screen or presses Esc.

In order to show a notification, inject the Notifications bean into the screen controller and use its fluent interface. In the following example, a notification is shown on the button click:

@Inject
private Notifications notifications;

@Subscribe("sayHelloBtn")
protected void onSayHelloBtnClick(Button.ClickEvent event) {
    notifications.create().withCaption("Hello!").show();
}

A notification can have a description which is displayed under the caption in a lighter font:

@Inject
private Notifications notifications;

@Subscribe("sayHelloBtn")
protected void onSayHelloBtnClick(Button.ClickEvent event) {
    notifications.create().withCaption("Greeting").withDescription("Hello World!").show();
}

Notifications can be of the following types:

  • TRAY - a notification is displayed in the bottom right corner of the application and disappears automatically.

  • HUMANIZED – a standard notification displayed in the center of the screen, disappears automatically.

  • WARNING – a warning. Disappears when user clicks on the screen.

  • ERROR– a notification about an error. Disappears when user clicks on the screen.

The default type is HUMANIZED. You can provide another type in the create() method:

@Inject
private Notifications notifications;

@Subscribe("sayHelloBtn")
protected void onSayHelloBtnClick(Button.ClickEvent event) {
    notifications.create(Notifications.NotificationType.TRAY).withCaption("Hello World!").show();
}

You can use \n characters for line breaks in messages. In order to show HTML, use the withContentMode() method:

@Inject
private Notifications notifications;

@Subscribe("sayHelloBtn")
protected void onSayHelloBtnClick(Button.ClickEvent event) {
    notifications.create()
            .withContentMode(ContentMode.HTML)
            .withCaption("<i>Hello World!</i>")
            .show();
}

When using HTML, don’t forget to escape data to prevent code injection.

The other methods such as withHideDelayMs(), withPosition() and withStyleName() allow you to customize the notification look and behavior.

3.5.8. Background Tasks

Background tasks mechanism is designed for performing tasks at the client tier asynchronously without blocking the user interface.

In order to use background tasks, do the following:

  1. Define a task as an inheritor of the BackgroundTask abstract class. Pass a link to a screen controller which will be associated with the task and the task timeout to the task constructor.

    Closing the screen will interrupt the tasks associated with it. Additionally, the task will be interrupted automatically after the specified timeout.

    Actual actions performed by the task are implemented in the run() method.

  2. Create an object of BackgroundTaskHandler class controlling the task by passing the task instance to the handle() method of the BackgroundWorker bean. A link to BackgroundWorker can be obtained by an injection in a screen controller, or through the AppBeans class.

  3. Run the task by invoking the execute() method of BackgroundTaskHandler.

UI components' state and data containers must not be read/updated from BackgroundTask run() method: use done(), progress(), and canceled() callbacks instead. An IllegalConcurrentAccessException is thrown in case you try to set a value to UI component from a background thread.

Example:

@Inject
protected BackgroundWorker backgroundWorker;

@Override
public void init(Map<String, Object> params) {
    // Create task with 10 sec timeout and this screen as owner
    BackgroundTask<Integer, Void> task = new BackgroundTask<Integer, Void>(10, this) {
        @Override
        public Void run(TaskLifeCycle<Integer> taskLifeCycle) throws Exception {
            // Do something in background thread
            for (int i = 0; i < 5; i++) {
                TimeUnit.SECONDS.sleep(1); // time consuming computations
                taskLifeCycle.publish(i);  // publish current progress to show it in progress() method
            }
            return null;
        }

        @Override
        public void canceled() {
            // Do something in UI thread if the task is canceled
        }

        @Override
        public void done(Void result) {
            // Do something in UI thread when the task is done
        }

        @Override
        public void progress(List<Integer> changes) {
            // Show current progress in UI thread
        }
    };
    // Get task handler object and run the task
    BackgroundTaskHandler taskHandler = backgroundWorker.handle(task);
    taskHandler.execute();
}

Detailed information about methods is provided in JavaDocs for BackgroundTask, TaskLifeCycle, BackgroundTaskHandler classes.

Please note the following:

  • BackgroundTask<T, V> is a parameterized class:

    • T − the type of objects displaying task progress. Objects of this type are passed to the task’s progress() method during an invocation of TaskLifeCycle.publish() in the working thread.

    • V − task result type passed to the done() method. It can also be obtained by invoking BackgroundTaskHandler.getResult() method, which will wait for a task to complete.

  • canceled() method is invoked only during a controlled cancellation of a task, i.e. when cancel() is invoked in the TaskHandler.

  • handleTimeoutException() is invoked when the task timeout expires. If the window where the task is running closes, the task is stopped without a notification.

  • run() method of a task should support external interruptions. To ensure this, we recommend checking the TaskLifeCycle.isInterrupted() flag periodically during long processes and stopping execution when needed. Additionally, you should not silently discard InterruptedException (or any other exception) - instead you should either exit the method correctly or not handle the exception at all.

    • isCancelled() method returns true if a task was interrupted by calling the cancel() method.

      public String run(TaskLifeCycle<Integer> taskLifeCycle) {
          for (int i = 0; i < 9_000_000; i++) {
              if (taskLifeCycle.isCancelled()) {
                  log.info(" >>> Task was cancelled");
                  break;
              } else {
                  log.info(" >>> Task is working: iteration #" + i);
              }
          }
          return "Done";
      }
  • BackgroundTask objects are stateless. If you did not create fields for temporary data when implementing task class, you can start several parallel processes using a single task instance.

  • BackgroundHandler object (its execute() method) can only be started once. If you need to restart a task frequently, use BackgroundTaskWrapper class.

  • Use BackgroundWorkWindow or BackgroundWorkProgressWindow classes with a set of static methods to show a modal window with progress indicator and Cancel button. You can define progress indication type and allow or prohibit cancellation of the background task for the window.

  • If you need to use certain values of visual components in the task thread, you should implement their acquisition in getParams() method, which runs in the UI thread once, when a task starts. In the run() method, these parameters will be accessible via the getParams() method of the TaskLifeCycle object.

  • If any exception occurs, the framework invokes BackgroundTask.handleException() method in the UI thread, which can be used to display the error.

  • Background tasks are affected by cuba.backgroundWorker.maxActiveTasksCount and cuba.backgroundWorker.timeoutCheckInterval application properties.

In Web Client, background tasks are implemented using HTTP push provided by the Vaadin framework. See https://vaadin.com/wiki/-/wiki/Main/Working+around+push+issues for information on how to set up your web servers for this technology.

If you don’t use background tasks, but want to update UI state from a non-UI thread, use methods of the UIAccessor interface. You should get a reference to UIAccessor using the BackgroundWorker.getUIAccessor() method in the UI thread, and after that you can invoke its access() and accessSynchronously() methods from a background thread to safely read or modify the state of UI components.

3.5.8.1. Background Task Usage Examples
Display and control background task operation with BackgroundWorkProgressWindow

Often when launching a background task one needs to display a simple UI:

  1. to show to the user that requested action is in the process of execution,

  2. to allow user to abort requested long operation,

  3. to show operation progress if progress percent can be determined.

Platform satisfies these needs with BackgroundWorkWindow and BackgroundWorkProgressWindow utility classes. These classes have static methods allowing to associate background task with a modal window that has a title, description, progress bar and optional Cancel button. The difference between two classes is that BackgroundWorkProgressWindow uses determinate progress bar, and it should be used in case if you can estimate progress of the task. Conversely, BackgroundWorkWindow should be used for tasks of indeterminate duration.

Consider the following development task as an example:

  • A given screen contains a table displaying a list of students, with multi-selection enabled.

  • When the user presses a button, the system should send reminder emails to selected students, without blocking UI and with an ability to cancel the operation.

bg task emails

Sample implementation:

import com.haulmont.cuba.gui.backgroundwork.BackgroundWorkProgressWindow;

public class StudentBrowse extends StandardLookup<Student> {

    @Inject
    private Table<Student> studentsTable;

    @Inject
    private EmailService emailService;

    @Subscribe("studentsTable.sendEmail")
    public void onStudentsTableSendEmail(Action.ActionPerformedEvent event) {
        Set<Student> selected = studentsTable.getSelected();
        if (selected.isEmpty()) {
            return;
        }
        BackgroundTask<Integer, Void> task = new EmailTask(selected);
        BackgroundWorkProgressWindow.show(task, (1)
                "Sending reminder emails", "Please wait while emails are being sent",
                selected.size(), true, true (2)
        );
    }

    private class EmailTask extends BackgroundTask<Integer, Void> { (3)
        private Set<Student> students; (4)

        public EmailTask(Set<Student> students) {
            super(10, TimeUnit.MINUTES, StudentBrowse.this); (5)
            this.students = students;
        }

        @Override
        public Void run(TaskLifeCycle<Integer> taskLifeCycle) throws Exception {
            int i = 0;
            for (Student student : students) {
                if (taskLifeCycle.isCancelled()) { (6)
                    break;
                }
                emailService.sendEmail(student.getEmail(), "Reminder", "Don't forget, the exam is tomorrow",
                        EmailInfo.TEXT_CONTENT_TYPE);

                i++;
                taskLifeCycle.publish(i); (7)
            }
            return null;
        }
    }
}
1 - launch the task and show modal progress window
2 - set dialog options: total number of elements for progress bar, user can cancel a task, show progress percent
3 - task progress unit is Integer (number of processed table items), and result type is Void because this task doesn’t produce result
4 - selected table items are saved into a variable which is initialized in the task constructor. This is necessary because run() method is executed in a background thread and cannot access UI components.
5 - set timeout to 10 minutes
6 - periodically check isCancelled() so that the task can stop immediately after the user pressed Cancel dialog button
7 - update progress bar position after every email sent
Periodically refresh screen data in the background using Timer and BackgroundTaskWrapper

BackgroundTaskWrapper is a tiny utility wrapper around BackgroundWorker. It provides simple API for cases when background tasks of the same type get started, restarted and cancelled repetitively.

As a usage example, consider the following development task:

  • A rank monitoring screen needs to display and automatically update some data.

  • Data is loaded slowly and therefore it should be loaded in the background.

  • Show time of the latest data update on the screen.

  • Data is filtered with simple filter (checkbox).

bg ranks ok
  • If data refresh fails for some reason, the screen should indicate this fact to the user:

bg ranks error

Sample implementation:

@UiController("playground_RankMonitor")
@UiDescriptor("rank-monitor.xml")
public class RankMonitor extends Screen {
    @Inject
    private Notifications notifications;
    @Inject
    private Label<String> refreshTimeLabel;
    @Inject
    private CollectionContainer<Rank> ranksDc;
    @Inject
    private RankService rankService;
    @Inject
    private CheckBox onlyActiveBox;
    @Inject
    private Logger log;
    @Inject
    private TimeSource timeSource;
    @Inject
    private Timer refreshTimer;

    private BackgroundTaskWrapper<Void, List<Rank>> refreshTaskWrapper = new BackgroundTaskWrapper<>(); (1)

    @Subscribe
    public void onBeforeShow(BeforeShowEvent event) {
        refreshTimer.setDelay(5000);
        refreshTimer.setRepeating(true);
        refreshTimer.start();
    }

    @Subscribe("onlyActiveBox")
    public void onOnlyActiveBoxValueChange(HasValue.ValueChangeEvent<Boolean> event) {
        refreshTaskWrapper.restart(new RefreshScreenTask()); (2)
    }

    @Subscribe("refreshTimer")
    public void onRefreshTimerTimerAction(Timer.TimerActionEvent event) {
        refreshTaskWrapper.restart(new RefreshScreenTask()); (3)
    }

    public class RefreshScreenTask extends BackgroundTask<Void, List<Rank>> { (4)
        private boolean onlyActive; (5)
        protected RefreshScreenTask() {
            super(30, TimeUnit.SECONDS, RankMonitor.this);
            onlyActive = onlyActiveBox.getValue();
        }

        @Override
        public List<Rank> run(TaskLifeCycle<Void> taskLifeCycle) throws Exception {
            List<Rank> data = rankService.loadActiveRanks(onlyActive); (6)
            return data;
        }

        @Override
        public void done(List<Rank> result) { (7)
            List<Rank> mutableItems = ranksDc.getMutableItems();
            mutableItems.clear();
            mutableItems.addAll(result);

            String hhmmss = new SimpleDateFormat("HH:mm:ss").format(timeSource.currentTimestamp());
            refreshTimeLabel.setValue("Last time refreshed: " + hhmmss);
        }

        @Override
        public boolean handleTimeoutException() { (8)
            displayRefreshProblem();
            return true;
        }

        @Override
        public boolean handleException(Exception ex) { (9)
            log.debug("Auto-refresh error", ex);
            displayRefreshProblem();
            return true;
        }

        private void displayRefreshProblem() {
            if (!refreshTimeLabel.getValue().endsWith("(outdated)")) {
                refreshTimeLabel.setValue(refreshTimeLabel.getValue() + " (outdated)");
            }
            notifications.create(Notifications.NotificationType.TRAY)
                    .withCaption("Problem refreshing data")
                    .withHideDelayMs(10_000)
                    .show();
        }
    }
}
1 - initialize BackgroundTaskWrapper instance with no-arg constructor; for every iteration a new task instance will be supplied
2 - immediately trigger a background data refresh after checkbox value has changed
3 - every timer tick triggers a data refresh in the background
4 - task publishes no progress so progress unit is Void; task produces result of type List<Rank>
5 - checkbox state is saved into a variable which is initialized in the task constructor. This is necessary because run() method is executed in a background thread and cannot access UI components.
6 - call custom service to load data (this is the long operation to be executed in the background)
7 - apply successfully obtained result to screen’s components
8 - update UI in the special case if data loading timed out: show notification in the screen corner
9 - inform user that data loading has failed with exception by showing notification

3.5.9. Themes

Themes are used to manage the visual presentation of the application.

A web theme consists of SCSS files and other resources like images.

The platform provides several themes that can be used in the project out-of-the-box. A theme extension allows you to modify an existing theme on the project level. You can also create your own custom themes in addition to the standard ones.

If you want to use a theme in multiple projects, you can include it in an application component or create a reusable theme JAR.

3.5.9.1. Using Existing Themes

The platform includes three ready-to-use themes: Hover, Halo and Havana. By default, the application will use the one specified in the cuba.web.theme application property.

The user may select the other theme in the standard Help > Settings screen. If you want to disable the option of selecting themes, register the settings screen in the web-screens.xml file of your project and set the changeThemeEnabled = false parameter for it:

<screen id="settings" template="/com/haulmont/cuba/web/app/ui/core/settings/settings-window.xml">
    <param name="changeThemeEnabled" value="false"/>
</screen>
3.5.9.2. Extending an Existing Theme

A platform theme can be modified in the project. In the modified theme, you can:

  • Change branding images.

  • Add icons to use them in visual components. See the Icons section below.

  • Create new styles for visual components and use them in the stylename attribute. This requires some expertise in CSS.

  • Modify existing styles of the visual components.

  • Modify common parameters, such as background color, margins, spacing, etc.

File structure and build scripts

Themes are defined in SCSS files. To modify (extend) a theme in the project, you should create a specific file structure in the web module.

A convenient way to do this is to use CUBA Studio: in the main menu, click CUBA > Advanced > Manage themes > Create theme extension. Select the theme you want to extend in the popup window. Another way is to use the theme command in CUBA CLI.

As a result, the following directory structure will be created in the modules/web directory (for Halo theme extension):

themes/
  halo/
    branding/
        app-icon-login.png
        app-icon-menu.png
    com.company.application/
        app-component.scss
        halo-ext.scss
        halo-ext-defaults.scss
    favicon.ico
    styles.scss

Apart from that, the build.gradle script will be complemented with the buildScssThemes task, which is executed automatically each time the web module is built. The optional deployThemes task can be used to quickly apply changes in themes to the running application.

If your project contains an application component with extended theme, and you want this extension to be used for the whole project, then you should create theme extension for the project too. For more details on how to inherit the component’s theme, see the Using Themes from Application Components section.

Changing branding

You can configure some branding properties, such as icons, login and main application window captions, and the website icon (favicon.ico).

To use custom images, replace default ones in the modules/web/themes/halo/branding directory.

To set window captions and the login window welcome text, set window captions and the login window welcome text in main message pack of the web module (i.e the modules/web/<root_package>/web/messages.properties file and its variants for different locales). Message packs allow you to use different image files for different user locales. The sample messages.properties file:

application.caption = MyApp
application.logoImage = branding/myapp-menu.png

loginWindow.caption = MyApp Login
loginWindow.welcomeLabel = Welcome to MyApp!
loginWindow.logoImage = branding/myapp-login.png

The path to favicon.ico is not specified since it must be located in the root directory of the theme.

Adding fonts

You can add custom fonts to your web theme. To add a font family, import it in the first line of the styles.scss file, for example:

@import url(http://fonts.googleapis.com/css?family=Roboto);
Creating new styles

Consider the example of setting the yellow background color to the field displaying the customer’s name.

In an XML descriptor, the FieldGroup component is defined:

<fieldGroup id="fieldGroup" datasource="customerDs">
    <field property="name"/>
    <field property="address"/>
</fieldGroup>

The field elements of FieldGroup do not have the stylename attribute, therefore we have to set the field’s style name in the controller:

@Named("fieldGroup.name")
private TextField nameField;

@Override
public void init(Map<String, Object> params) {
    nameField.setStyleName("name-field");
}

In the halo-ext.scss file, add the new style definition to the halo-ext mixin:

@mixin com_company_application-halo-ext {
  .name-field {
    background-color: lightyellow;
  }
}

After rebuilding the project, the fields will look as follows:

gui themes fieldgroup 1
Modifying existing styles of the visual components

To modify style parameters of existing components, add the corresponding CSS code to the halo-ext mixin of the halo-ext.scss file. Use developer tools of your web browser to find out CSS classes assigned to the elements of visual components. For example, to display the application menu items in bold, the contents of the halo-ext.scss file should be as follows:

@mixin com_company_application-halo-ext {
  .v-menubar-menuitem-caption {
      font-weight: bold;
  }
}
Modifying common parameters

Themes contain a number of SCSS variables that control application background colour, component size, margins and other parameters.

Below is the example of a Halo theme extension, since it is based on Valo theme from Vaadin, and provides the widest range of options for customization.

The themes/halo/halo-ext-defaults.scss file is intended for overriding theme variables. Most of the Halo variables correspond to those described in the Valo documentation. Below are the most common variables:

$v-background-color: #fafafa;        /* component background colour */
$v-app-background-color: #e7ebf2;    /* application background colour */
$v-panel-background-color: #fff;     /* panel background colour */
$v-focus-color: #3b5998;             /* focused element colour */
$v-error-indicator-color: #ed473b;   /* empty required fields colour */

$v-line-height: 1.35;                /* line height */
$v-font-size: 14px;                  /* font size */
$v-font-weight: 400;                 /* font weight */
$v-unit-size: 30px;                  /* base theme size, defines the height for buttons, fields and other elements */

$v-font-size--h1: 24px;              /* h1-style Label size */
$v-font-size--h2: 20px;              /* h2-style Label size */
$v-font-size--h3: 16px;              /* h3-style Label size */

/* margins for containers */
$v-layout-margin-top: 10px;
$v-layout-margin-left: 10px;
$v-layout-margin-right: 10px;
$v-layout-margin-bottom: 10px;

/* spacing between components in a container (if enabled) */
$v-layout-spacing-vertical: 10px;
$v-layout-spacing-horizontal: 10px;

/* whether filter search button should have "friendly" style*/
$cuba-filter-friendly-search-button: true;

/* whether button that has primary action or marked as primary itself should be highlighted*/
$cuba-highlight-primary-action: false;

/* basic table and datagrid settings */
$v-table-row-height: 30px;
$v-table-header-font-size: 13px;
$v-table-cell-padding-horizontal: 7px;
$v-grid-row-height
$v-grid-row-selected-background-color
$v-grid-cell-padding-horizontal

/* input field focus style */
$v-focus-style: inset 0px 0px 5px 1px rgba($v-focus-color, 0.5);
/* required fields focus style */
$v-error-focus-style: inset 0px 0px 5px 1px rgba($v-error-indicator-color, 0.5);

/* animation for elements is enabled by default */
$v-animations-enabled: true;
/* popup window animation is disabled by default */
$v-window-animations-enabled: false;

/* inverse header is controlled by cuba.web.useInverseHeader property */
$v-support-inverse-menu: true;

/* show "required" indicators for components */
$v-show-required-indicators: false !default;

The sample halo-ext-defaults.scss for a theme with a dark background and slightly minimized margins is provided below:

$v-background-color: #444D50;

$v-font-size--h1: 22px;
$v-font-size--h2: 18px;
$v-font-size--h3: 16px;

$v-layout-margin-top: 8px;
$v-layout-margin-left: 8px;
$v-layout-margin-right: 8px;
$v-layout-margin-bottom: 8px;

$v-layout-spacing-vertical: 8px;
$v-layout-spacing-horizontal: 8px;

$v-table-row-height: 25px;
$v-table-header-font-size: 13px;
$v-table-cell-padding-horizontal: 5px;

$v-support-inverse-menu: false;

Another example is a set of variables that makes Halo theme look like the old Havana theme removed from the framework version 7:

$cuba-menubar-background-color: #315379;
$cuba-menubar-border-color: #315379;
$v-table-row-height: 25px;
$v-selection-color: rgb(77, 122, 178);
$v-table-header-font-size: 12px;
$v-textfield-border: 1px solid #A5C4E0;

$v-selection-item-selection-color: #4D7AB2;

$v-app-background-color: #E3EAF1;
$v-font-size: 12px;
$v-font-weight: 400;
$v-unit-size: 25px;
$v-border-radius: 0px;
$v-border: 1px solid #9BB3D3 !default;
$v-font-family: Verdana,tahoma,arial,geneva,helvetica,sans-serif,"Trebuchet MS";

$v-panel-background-color: #ffffff;
$v-background-color: #ffffff;

$cuba-menubar-menuitem-text-color: #ffffff;

$cuba-app-menubar-padding-top: 8px;
$cuba-app-menubar-padding-bottom: 8px;

$cuba-menubar-text-color: #ffffff;
$cuba-menubar-submenu-padding: 1px;
Changing the application header

Halo theme supports the cuba.web.useInverseHeader property, which controls the colour of the application header. By default, this property is set to true, which sets a dark (inverse) header.You can make a light header without any changes to the theme, simply by setting this property to false.

3.5.9.3. Creating a Custom Theme

You can create one or several application themes in the project and give the users an opportunity to select the most appropriate one. Creating new themes also allows you to override the variables in the *-theme.properties files, which define a few server-side parameters:

  • Default dialog window size.

  • Default input field width.

  • Dimensions of some components (Filter, FileMultiUploadField).

  • Correspondence between icon names and constants of the com.vaadin.server.FontAwesome enumeration for using Font Awesome in standard actions and screens of the platform, if cuba.web.useFontIcons is enabled.

New themes can be easily created in CUBA Studio, in CUBA CLI or manually. Let’s consider all the three ways taking Hover Dark custom theme as an example.

In CUBA Studio:
  • In the main menu, click CUBA > Advanced > Manage themes > Create custom theme. Enter the name of the new theme: hover-dark. Select the hover theme in the Base theme dropdown.

    The required file structure will be created in the web module. The webThemesModule module and its configuration will be automatically added to the settings.gradle and build.gradle files. Also, the generated deployThemes gradle task allows you to see the changes without server restart.

Manually:
  • Create the following file structure in the web module of your project:

    web/
      src/
      themes/
        hover-dark/
          branding/
              app-icon-login.png
              app-icon-menu.png
          com.haulmont.cuba/
              app-component.scss
          favicon.ico
          hover-dark.scss
          hover-dark-defaults.scss
          styles.scss
  • The app-component.scss file:

    @import "../hover-dark";
    
    @mixin com_haulmont_cuba {
      @include hover-dark;
    }
  • The hover-dark.scss file:

    @import "../hover/hover";
    
    @mixin hover-dark {
      @include hover;
    }
  • The styles.scss file:

    @import "hover-dark-defaults";
    @import "hover-dark";
    
    .hover-dark {
      @include hover-dark;
    }
  • Create the hover-dark-theme.properties file in the web subdirectory of your web module:

    @include=hover-theme.properties
  • Add the webThemesModule module to the settings.gradle file:

    include(":${modulePrefix}-global", ":${modulePrefix}-core", ":${modulePrefix}-web", ":${modulePrefix}-web-themes")
    //...
    project(":${modulePrefix}-web-themes").projectDir = new File(settingsDir, 'modules/web/themes')
  • Add the webThemesModule module configuration to the build.gradle file:

    def webThemesModule = project(":${modulePrefix}-web-themes")
    
    configure(webThemesModule) {
      apply(plugin: 'java')
      apply(plugin: 'maven')
      apply(plugin: 'cuba')
    
      appModuleType = 'web-themes'
    
      buildDir = file('../build/scss-themes')
    
      sourceSets {
        main {
          java {
            srcDir '.'
          }
          resources {
            srcDir '.'
          }
        }
      }
    }
  • Finally, create the deployThemes gradle task in build.gradle to see the changes without server restart:

    configure(webModule) {
      // . . .
      task buildScssThemes(type: CubaWebScssThemeCreation)
      task deployThemes(type: CubaDeployThemeTask, dependsOn: buildScssThemes)
      assemble.dependsOn buildScssThemes
    }
In CUBA CLI:
  • Run the theme command, then select the hover theme.

    The specific file structure will be created in the web module of the project.

  • Modify the generated file structure and the files' contents so that they correspond to the files from above.

  • Create the hover-dark-theme.properties file in the source directory of your web module:

    @include=hover-theme.properties

The build.gradle and settings.gradle files will be updated automatically by CLI.

See also the example in Creating Facebook Theme section.

Modifying server-side theme parameters

In Halo theme, Font Awesome icons are used for standard actions and platform screens by default (if cuba.web.useFontIcons is enabled). In this case, you can replace a standard icon by setting the required mapping between the icon and the font element name in <your_theme>-theme.properties file. For example, to use "plus" icon for the create action in the new Facebook theme, the facebook-theme.properties file should contain the following:

@include=halo-theme.properties

cuba.web.icons.create.png = font-icon:PLUS

The fragment of the standard users browser screen in the Facebook theme with the modified create action:

gui theme facebook 1
3.5.9.3.1. Creating Hover Dark Theme

Here you can find the steps to create a Hover Dark theme, which will be a dark variation of the default Hover theme. The sample application with this theme is available on GitHub.

  1. Create the new hover-dark theme in your project following the instructions in Creating a Custom Theme section.

    The required file structure will be created in the web module. The webThemesModule module and its configuration will be automatically added to the settings.gradle and build.gradle files.

  2. Override the default style variables in the hover-dark-defaults.scss file, i.e. replace the variables in it by the following ones:

    @import "../hover/hover-defaults";
    
    $v-app-background-color: #262626;
    $v-background-color: lighten($v-app-background-color, 12%);
    $v-border: 1px solid (v-tint 0.8);
    $font-color: valo-font-color($v-background-color, 0.85);
    $v-button-font-color: $font-color;
    $v-font-color: $font-color;
    $v-link-font-color: lighten($v-focus-color, 15%);
    $v-link-text-decoration: none;
    $v-textfield-background-color: $v-background-color;
    
    $cuba-hover-color: #75a4c1;
    $cuba-maintabsheet-tabcontainer-background-color: $v-app-background-color;
    $cuba-menubar-background-color: lighten($v-app-background-color, 4%);
    $cuba-tabsheet-tab-caption-selected-color: $v-font-color;
    $cuba-window-modal-header-background: $v-background-color;
    
    $cuba-menubar-menuitem-border-radius: 0;
  3. Using the cuba.themeConfig application property define the themes you want to make available in the application:

    cuba.themeConfig = com/haulmont/cuba/hover-theme.properties /com/company/demo/web/hover-dark-theme.properties

As the result, both themes will be available in the application: the default Hover theme and its dark variation.

hover dark
3.5.9.3.2. Creating Facebook Theme

Below is the example of creating a Halo-based Facebook theme, which resembles the interface of a popular social network.

  1. In CUBA Studio, click CUBA > Advanced > Manage themes > Create custom theme. Set the theme name - facebook, select halo as the base theme and click Create. The new theme directory will be created in the project:

    themes/
        facebook/
            branding/
                app-icon-login.png
                app-icon-menu.png
            com.haulmont.cuba/
                app-component.scss                  // cuba app-component include
            facebook.scss                           // main theme file
            facebook-defaults.scss                  // main theme variables
            favicon.ico
            styles.scss                             // entry point of SCSS build procedure

    The styles.scss file contains the list of your themes:

    @import "facebook-defaults";
    @import "facebook";
    
    .facebook {
      @include facebook;
    }

    The facebook.scss file:

    @import "../halo/halo";
    
    @mixin facebook {
      @include halo;
    }

    The app-component.scss file inside com.haulmont.cuba:

    @import "../facebook";
    
    @mixin com_haulmont_cuba {
      @include facebook;
    }
  2. Modify the theme variables in facebook-defaults.scss. You can do it in Studio by clicking Manage themes > Edit Facebook theme variables or in IDE:

    @import "../halo/halo-defaults";
    
    $v-background-color: #fafafa;
    $v-app-background-color: #e7ebf2;
    $v-panel-background-color: #fff;
    $v-focus-color: #3b5998;
    
    $v-border-radius: 0;
    $v-textfield-border-radius: 0;
    
    $v-font-family: Helvetica, Arial, 'lucida grande', tahoma, verdana, arial, sans-serif;
    $v-font-size: 14px;
    $v-font-color: #37404E;
    $v-font-weight: 400;
    
    $v-link-text-decoration: none;
    $v-shadow: 0 1px 0 (v-shade 0.2);
    $v-bevel: inset 0 1px 0 v-tint;
    $v-unit-size: 30px;
    $v-gradient: v-linear 12%;
    $v-overlay-shadow: 0 3px 8px v-shade, 0 0 0 1px (v-shade 0.7);
    $v-shadow-opacity: 20%;
    $v-selection-overlay-padding-horizontal: 0;
    $v-selection-overlay-padding-vertical: 6px;
    $v-selection-item-border-radius: 0;
    
    $v-line-height: 1.35;
    $v-font-size: 14px;
    $v-font-weight: 400;
    $v-unit-size: 25px;
    
    $v-font-size--h1: 22px;
    $v-font-size--h2: 18px;
    $v-font-size--h3: 16px;
    
    $v-layout-margin-top: 8px;
    $v-layout-margin-left: 8px;
    $v-layout-margin-right: 8px;
    $v-layout-margin-bottom: 8px;
    
    $v-layout-spacing-vertical: 8px;
    $v-layout-spacing-horizontal: 8px;
    
    $v-table-row-height: 25px;
    $v-table-header-font-size: 13px;
    $v-table-cell-padding-horizontal: 5px;
    
    $v-focus-style: inset 0px 0px 1px 1px rgba($v-focus-color, 0.5);
    $v-error-focus-style: inset 0px 0px 1px 1px rgba($v-error-indicator-color, 0.5);
  3. The facebook-theme.properties file in the src directory of the web module can be used to override the server-side theme variables from the halo-theme.properties file of the platform.

  4. The new theme has been automatically added to the web-app.properties file:

    cuba.web.theme = facebook
    cuba.themeConfig = com/haulmont/cuba/halo-theme.properties /com/company/application/web/facebook-theme.properties

    The cuba.themeConfig property defines which themes will be available for the user in the Settings menu of an application.

  5. Rebuild the application and start the server. Now the user will see the application in Facebook theme on first login, and will be able to choose between Facebook, Halo and Havana in the Help > Settings menu.

facebook theme
3.5.9.4. Using Themes from Application Components

If your project contains an application component with a custom theme, you can use this theme for the whole project.

To inherit the theme as is, simply add it to the cuba.themeConfig application property:

cuba.web.theme = {theme-name}
cuba.themeConfig = com/haulmont/cuba/hover-theme.properties /com/company/{app-component-name}/{theme-name}-theme.properties

In case you want to override some variables from the parent theme, you need to create a theme extension in your project first.

In the following example we will use the facebook theme from the Creating a Custom Theme section.

  1. Follow the steps to create the facebook theme for your app component.

  2. Install the app component using Studio menu as described in the Example of Application Component section.

  3. Extend the halo theme in the project that uses your application component.

  4. By means of your IDE, rename all halo occurrences in themes directory, including file names, to facebook in order to get the following structure:

    themes/
        facebook/
            branding/
                app-icon-login.png
                app-icon-menu.png
            com.company.application/
                app-component.scss
                facebook-ext.scss
                facebook-ext-defaults.scss
            favicon.ico
            styles.scss
  5. The app-component.scss file aggregates theme modifications of the application component. During the SCSS build process, the Gradle plugin automatically finds the app components and imports them in the generated modules/web/build/themes-tmp/VAADIN/themes/{theme-name}/app-components.scss file.

    By default app-component.scss does not include variables modifications from {theme-name}-ext-defaults. To include variables modifications to app component bundle, you should import it manually in app-component.scss:

    @import "facebook-ext";
    @import "facebook-ext-defaults";
    
    @mixin com_company_application {
      @include com_company_application-facebook-ext;
    }

    At this stage the facebook theme is already imported from the app component to the project.

  6. Now you can use facebook-ext.scss and facebook-ext-defaults.scss files inside the com.company.application package to override variables from app component’s theme and customize it for the concrete project.

  7. Add the following properties to the web-app.properties file to make the facebook theme available in the Settings menu of the application. Use the relative path to reference facebook-theme.properties from the app component.

    cuba.web.theme = facebook
    cuba.themeConfig = com/haulmont/cuba/hover-theme.properties /com/company/{app-component-name}/facebook-theme.properties

In case of any trouble with themes building check modules/web/build/themes-tmp directory. It contains all the files and generated app-component.scss includes, thus enabling you to investigate SCSS compilation problems.

3.5.9.5. Creating a Reusable Theme

Any theme can be packed and reused without an application component. To create a theme package, you need to create a Java project from scratch and bundle it in a single JAR file. Follow the steps below to create a distribution of facebook theme from the previous examples.

  1. Create a new project with the following structure in IDE. It will be a simple Java project that consists of SCSS files and theme properties:

    halo-facebook/
        src/                                            //sources root
            halo-facebook/
                com.haulmont.cuba/
                    app-component.scss
                halo-facebook.scss
                halo-facebook-defaults.scss
                halo-facebook-theme.properties
                styles.scss

    This sample theme project can be downloaded from GitHub.

    • build.gradle script:

      allprojects {
          group = 'com.haulmont.theme'
          version = '0.1'
      }
      
      apply(plugin: 'java')
      apply(plugin: 'maven')
      
      sourceSets {
          main {
              java {
                  srcDir 'src'
              }
              resources {
                  srcDir 'src'
              }
          }
      }
    • settings.gradle file:

      rootProject.name = 'halo-facebook'
    • app-component.scss file:

      @import "../halo-facebook";
      
      @mixin com_haulmont_cuba {
        @include halo-facebook;
      }
    • halo-facebook.scss file:

      @import "../@import "../";
      
      @mixin halo-facebook {
        @include halo;
      }
    • halo-facebook-defaults.scss file:

      @import "../halo/halo-defaults";
      
      $v-background-color: #fafafa;
      $v-app-background-color: #e7ebf2;
      $v-panel-background-color: #fff;
      $v-focus-color: #3b5998;
      $v-border-radius: 0;
      $v-textfield-border-radius: 0;
      $v-font-family: Helvetica, Arial, 'lucida grande', tahoma, verdana, arial, sans-serif;
      $v-font-size: 14px;
      $v-font-color: #37404E;
      $v-font-weight: 400;
      $v-link-text-decoration: none;
      $v-shadow: 0 1px 0 (v-shade 0.2);
      $v-bevel: inset 0 1px 0 v-tint;
      $v-unit-size: 30px;
      $v-gradient: v-linear 12%;
      $v-overlay-shadow: 0 3px 8px v-shade, 0 0 0 1px (v-shade 0.7);
      $v-shadow-opacity: 20%;
      $v-selection-overlay-padding-horizontal: 0;
      $v-selection-overlay-padding-vertical: 6px;
      $v-selection-item-border-radius: 0;
      
      $v-line-height: 1.35;
      $v-font-size: 14px;
      $v-font-weight: 400;
      $v-unit-size: 25px;
      
      $v-font-size--h1: 22px;
      $v-font-size--h2: 18px;
      $v-font-size--h3: 16px;
      
      $v-layout-margin-top: 8px;
      $v-layout-margin-left: 8px;
      $v-layout-margin-right: 8px;
      $v-layout-margin-bottom: 8px;
      
      $v-layout-spacing-vertical: 8px;
      $v-layout-spacing-horizontal: 8px;
      
      $v-table-row-height: 25px;
      $v-table-header-font-size: 13px;
      $v-table-cell-padding-horizontal: 5px;
      
      $v-focus-style: inset 0px 0px 1px 1px rgba($v-focus-color, 0.5);
      $v-error-focus-style: inset 0px 0px 1px 1px rgba($v-error-indicator-color, 0.5);
      
      $v-show-required-indicators: true;
    • halo-facebook-theme.properties file:

      @include=halo-theme.properties
  2. Build and install the project with the Gradle task:

    gradle assemble install
  3. Add the theme to your CUBA-based project as a Maven dependency in two configurations: themes and compile, by modifying you build.gradle file:

    configure(webModule) {
        //...
        dependencies {
            provided(servletApi)
            compile(guiModule)
    
            compile('com.haulmont.theme:halo-facebook:0.1')
            themes('com.haulmont.theme:halo-facebook:0.1')
        }
        //...
    }

    If you install the theme locally, don’t forget to add mavenLocal() to the list of repositories: open the Project Properties section in Studio and add the local Maven repository coordinates to the repositories list.

  4. To inherit this theme and modify it in your project, you have to extend this theme. Extend the halo theme and rename themes/halo folder to themes/halo-facebook:

    themes/
        halo-facebook/
            branding/
                app-icon-login.png
                app-icon-menu.png
            com.company.application/
                app-component.scss
                halo-ext.scss
                halo-ext-defaults.scss
            favicon.ico
            styles.scss
  5. Modify styles.scss file:

    @import "halo-facebook-defaults";
    @import "com.company.application/halo-ext-defaults";
    @import "app-components";
    @import "com.company.application/halo-ext";
    
    .halo-facebook {
      // include auto-generated app components SCSS
      @include app_components;
    
      @include com_company_application-halo-ext;
    }
  6. The last step is to define halo-facebook-theme.properties file in web-app.properties file:

    cuba.themeConfig = com/haulmont/cuba/hover-theme.properties /halo-facebook/halo-facebook-theme.properties

Now, you can choose halo-facebook theme from Help > Settings menu or set the default theme using cuba.web.theme application property.

3.5.10. Icons

Image files used in the icon properties for actions and visual components, e.g. Button, can be added to your theme extension.

For example, to add an icon to the Halo theme extension, you have to add the image file to the modules/web/themes/halo directory described in the Extending an Existing Theme section (it is recommended to create a subfolder):

themes/
  halo/
    icons/
      cool-icon.png

In the following sections, we consider using the icons in visual components and adding icons from arbitrary font libraries.

3.5.10.1. Icon Sets

Icon sets allow you to decouple usage of icons in visual components from real paths to images in theme or font element constants. They also simplify overriding of icons used in the UI inherited from application components.

Icon sets are enumerations with items corresponding to icons. An icon set must implement the Icons.Icon interface which has one parameter: a string which denotes the source of an icon, for example, font-icon:CHECK or icons/myawesomeicon.png. To obtain the source, use the Icons bean provided by the platform.

Icon sets can be created in the web or gui module. All names of icon set items should match the regexp: [A-Z]_, i.e. they should contain only upper-case letters and underscores.

For example:

public enum MyIcon implements Icons.Icon {

    // adding new icon
    COOL_ICON("icons/cool-icon.png"),

    // overriding a CUBA default icon
    OK("icons/my-ok.png");

    protected String source;

    MyIcon(String source) {
        this.source = source;
    }

    @Override
    public String source() {
        return source;
    }
}

Icon sets should be registered in cuba.iconsConfig application property, e.g:

web-app.properties
cuba.iconsConfig = +com.company.demo.gui.icons.MyIcon

To make the icon set from an application component accessible in the target project, this property should be added to the component descriptor.

Now you can use the icons from this icon set simply by its name declaratively in screen XML descriptor:

<button icon="COOL_ICON"/>

or programmatically in the screen controller:

button.setIconFromSet(MyIcon.COOL_ICON);

The following prefixes allow you to use icons from different sources in declarative way:

  • theme - the icon will be served from the current theme directory, for example, web/themes/halo/awesomeFolder/superIcon.png:

    <button icon="theme:awesomeFolder/superIcon.png"/>
  • file - the icon will be served from file system:

    <button icon="file:D:/superIcon.png"/>
  • classpath - icon will be served from classpath, for example, com/company/demo/web/superIcon.png

    <button icon="classpath:/com/company/demo/web/superIcon.png"/>

There is one predefined icon set provided by the platform - CubaIcon. It includes almost full FontAwesome icon set and CUBA-specific icons. These icons can be selected in Studio icon editor:

icon set
3.5.10.2. Using Icons from Other Font Libraries

To enhance the theme extension, you may need to create icons and embed them into fonts, as well as use any external icons library.

  1. In the web module create the enum class implementing com.vaadin.server.FontIcon interface for the new icons:

    import com.vaadin.server.FontIcon;
    import com.vaadin.server.GenericFontIcon;
    
    public enum IcoMoon implements FontIcon {
    
        HEADPHONES(0XE900),
        SPINNER(0XE905);
    
        public static final String FONT_FAMILY = "IcoMoon";
        private int codepoint;
    
        IcoMoon(int codepoint) {
            this.codepoint = codepoint;
        }
    
        @Override
        public String getFontFamily() {
            return FONT_FAMILY;
        }
    
        @Override
        public int getCodepoint() {
            return codepoint;
        }
    
        @Override
        public String getHtml() {
            return GenericFontIcon.getHtml(FONT_FAMILY, codepoint);
        }
    
        @Override
        public String getMIMEType() {
            throw new UnsupportedOperationException(FontIcon.class.getSimpleName()
                    + " should not be used where a MIME type is needed.");
        }
    
        public static IcoMoon fromCodepoint(final int codepoint) {
            for (IcoMoon f : values()) {
                if (f.getCodepoint() == codepoint) {
                    return f;
                }
            }
            throw new IllegalArgumentException("Codepoint " + codepoint
                    + " not found in IcoMoon");
        }
    }
  2. Add new styles to the theme extension. We recommend creating a special subfolder fonts in the main folder of theme extension, for example, modules/web/themes/halo/com.company.demo/fonts. Put the styles and font files in their own subfolders, for example, fonts/icomoon.

    Files of fonts are represented by the following extensions:

    • .eot,

    • .svg,

    • .ttf,

    • .woff.

      The set of fonts icomoon from an open library, used in this example, consists of 4 joint used files: icomoon.eot, icomoon.svg, icomoon.ttf, icomoon.woff.

  3. Create a file with styles that includes @font-face and a CSS class with the icon style. Below is an example of the icomoon.scss file, where IcoMoon class name corresponds to the value returned by FontIcon#getFontFamily method:

    @mixin icomoon-style {
        /* use !important to prevent issues with browser extensions that change fonts */
        font-family: 'icomoon' !important;
        speak: none;
        font-style: normal;
        font-weight: normal;
        font-variant: normal;
        text-transform: none;
        line-height: 1;
    
        /* Better Font Rendering =========== */
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
    }
    
    @font-face {
        font-family: 'icomoon';
        src:url('icomoon.eot?hwgbks');
        src:url('icomoon.eot?hwgbks#iefix') format('embedded-opentype'),
            url('icomoon.ttf?hwgbks') format('truetype'),
            url('icomoon.woff?hwgbks') format('woff'),
            url('icomoon.svg?hwgbks#icomoon') format('svg');
        font-weight: normal;
        font-style: normal;
    }
    
    .IcoMoon {
        @include icomoon-style;
    }
  4. Create a reference to the file with font styles in halo-ext.scss or other file of theme extension:

    @import "fonts/icomoon/icomoon";
  5. Then create new icon set which is an enumeration implementing the Icons.Icon interface:

    import com.haulmont.cuba.gui.icons.Icons;
    
    public enum IcoMoonIcon implements Icons.Icon {
        HEADPHONES("ico-moon:HEADPHONES"),
        SPINNER("ico-moon:SPINNER");
    
        protected String source;
    
        IcoMoonIcon(String source) {
            this.source = source;
        }
    
        @Override
        public String source() {
            return source;
        }
    }
  6. Create new IconProvider.

    For managing custom icon sets CUBA platform provides the mechanism that consists of IconProvider and IconResolver.

    IconProvider is a marker interface that exists only in the web module and can provide resources (com.vaadin.server.Resource) by the icon path.

    The IconResolver bean obtains all beans that implement IconProvider interface and iterates over them to find the one that can provide a resource for the icon.

    In order to use this mechanism, you should create your implementation of IconProvider:

    import com.haulmont.cuba.web.gui.icons.IconProvider;
    import com.vaadin.server.Resource;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Component;
    
    @Order(10)
    @Component
    public class IcoMoonIconProvider implements IconProvider {
        private final Logger log = LoggerFactory.getLogger(IcoMoonIconProvider.class);
    
        @Override
        public Resource getIconResource(String iconPath) {
            Resource resource = null;
    
            iconPath = iconPath.split(":")[1];
    
            try {
                resource = ((Resource) IcoMoon.class
                        .getDeclaredField(iconPath)
                        .get(null));
            } catch (IllegalAccessException | NoSuchFieldException e) {
                log.warn("There is no icon with name {} in the FontAwesome icon set", iconPath);
            }
    
            return resource;
        }
    
        @Override
        public boolean canProvide(String iconPath) {
            return iconPath.startsWith("ico-moon:");
        }
    }

    Here we explicitly assign order for this bean with @Order annotation.

  7. Register your icon set in the application properties file:

    cuba.iconsConfig = +com.company.demo.gui.icons.IcoMoonIcon

Now you can use new icons by direct reference to their class and enum element in XML-descriptor of the screen:

<button caption="Headphones" icon="ico-moon:HEADPHONES"/>

or in the Java controller:

spinnerBtn.setIconFromSet("ico-moon:SPINNER");

As a result, new icons are added to the buttons:

add icons
Overriding icons with icon sets

The mechanism of icon sets enables you to override icons from other sets. In order to do this, you should create and register a new icon set (enumeration) with the same icons (options) but with different icon paths (source). In the following example the new MyIcon enum is created to override the standard icons from CubaIcon set.

  1. The default icon set:

    public enum CubaIcon implements Icons.Icon {
        OK("font-icon:CHECK"),
        CANCEL("font-icon:BAN"),
       ...
    }
  2. The new icon set:

    public enum MyIcon implements Icons.Icon {
        OK("icons/my-custom-ok.png"),
       ...
    }
  3. Register the new icon set in web-app.properties:

    cuba.iconsConfig = +com.company.demo.gui.icons.MyIcon

Now, the new OK icon will be used instead of the standard one:

Icons icons = AppBeans.get(Icons.NAME);
button.setIcon(icons.getIcon(CubaIcon.OK))

In case you need to ignore redefinitions, you still can use the standard icons by using the path to an icon instead of the option name:

<button caption="Created" icon="icons/create.png"/>

or

button.setIcon(CubaIcon.CREATE_ACTION.source());

3.5.11. DOM and CSS Attributes

The framework provides the ad-hoc HTML attributes API for setting DOM and CSS attributes to visual components.

The HtmlAttributes bean allows setting DOM/CSS attributes programmatically using the following methods:

  • setDomAttribute(Component component, String attributeName, String value) – sets DOM attribute on the top-most element of the UI component.

  • setCssProperty(Component component, String propertyName, String value) – sets CSS property value on the top-most element of UI component.

  • setDomAttribute(Component component, String querySelector, String attributeName, String value) – sets DOM attribute for all nested elements of UI component corresponding to the given query selector.

  • getDomAttribute(Component component, String querySelector, String attributeName) – gets DOM attribute value assigned earlier using HtmlAttributes. Does not reflect a real value from DOM.

  • removeDomAttribute(Component component, String querySelector, String attributeName) – removes DOM attribute for all nested elements of UI component corresponding to the given query selector.

  • setCssProperty(Component component, String querySelector, String propertyName, String value) – sets CSS property value for all nested elements of UI component corresponding to the given query selector.

  • getCssProperty(Component component, String querySelector, String propertyName) – gets CSS property value assigned earlier using HtmlAttributes. Does not reflect a real value from DOM.

  • removeCssProperty(Component component, String querySelector, String propertyName) – clears CSS property value for all nested elements of UI component corresponding to the given query selector.

  • applyCss(Component component, String querySelector, String css) – applies CSS properties from the CSS string.

The methods described above accept the following parameters:

  • component – the component’s identifier.

  • querySelector – a string containing one or more selectors to match. This string must be a valid CSS selector string.

  • attributeName – DOM attribute name (e.g. title).

  • propertyName – CSS property name (e.g. border-color).

  • value – the attribute value.

The most common DOM attribute names and CSS property names are available in the HtmlAttributes bean class as constants, but you can use any custom attributes as well.

The functioning of a particular attribute may vary depending on the component this attribute is applied to. Some visual components may implicitly use the same attributes for their own purposes, so the methods above may not work in some cases.

The HtmlAttributes bean should be injected in the screen controller and used as follows:

XML descriptor
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd"
        caption="Demo"
        messagesPack="com.company.demo.web">
    <layout>
        <button id="demoButton"
                caption="msg://demoButton"
                width="33%"/>
    </layout>
</window>
Screen controller
import com.haulmont.cuba.gui.components.Button;
import com.haulmont.cuba.gui.components.HtmlAttributes;
import com.haulmont.cuba.gui.screen.Screen;
import com.haulmont.cuba.gui.screen.Subscribe;
import com.haulmont.cuba.gui.screen.UiController;
import com.haulmont.cuba.gui.screen.UiDescriptor;

import javax.inject.Inject;

@UiController("demo_DemoScreen")
@UiDescriptor("demo-screen.xml")
public class DemoScreen extends Screen {

    @Inject
    private Button demoButton;

    @Inject
    protected HtmlAttributes html;

    @Subscribe
    private void onBeforeShow(BeforeShowEvent event) {
        html.setDomAttribute(demoButton, HtmlAttributes.DOM.TITLE, "Hello!");

        html.setCssProperty(demoButton, HtmlAttributes.CSS.BACKGROUND_COLOR, "red");
        html.setCssProperty(demoButton, HtmlAttributes.CSS.BACKGROUND_IMAGE, "none");
        html.setCssProperty(demoButton, HtmlAttributes.CSS.BOX_SHADOW, "none");
        html.setCssProperty(demoButton, HtmlAttributes.CSS.BORDER_COLOR, "red");
        html.setCssProperty(demoButton, "color", "white");

        html.setCssProperty(demoButton, HtmlAttributes.CSS.MAX_WIDTH, "400px");
    }
}

3.5.12. Keyboard Shortcuts

This section provides a list of keyboard shortcuts used in the generic user interface of the application. All the application properties listed below belong to the ClientConfig interface and can be used in Web Client block.

  • Main application window.

    • CTRL-SHIFT-PAGE_DOWN – switch to the next tab. Defined by the cuba.gui.nextTabShortcut property.

    • CTRL-SHIFT-PAGE_UP – switch to the previous tab. Defined by the cuba.gui.previousTabShortcut property.

  • Folders panel.

    • ENTER – open the selected folder.

    • SPACE - select/deselect the focused folder.

    • ARROW UP, ARROW DOWN - switch between folders.

    • ARROW LEFT, ARROW RIGHT - collapse/expand a folder with subfolders or jump to the owning folder.

  • Screens.

    • ESCAPE – close the current screen. Defined by the cuba.gui.closeShortcut property.

    • CTRL-ENTER – close the current editor and save the changes. Defined by the cuba.gui.commitShortcut property.

  • Standard actions for list components (Table, GroupTable, TreeTable, Tree). In addition to these application properties, a shortcut for a particular action can be set by calling it’s setShortcut() method.

    • CTRL-\ – call the CreateAction. Defined by the cuba.gui.tableShortcut.insert property.

    • CTRL-ALT-\ – call the AddAction. Defined by the cuba.gui.tableShortcut.add property.

    • ENTER – call the EditAction. Defined by the cuba.gui.tableShortcut.edit property.

    • CTRL-DELETE – call the RemoveAction and ExcludeAction. Defined by the cuba.gui.tableShortcut.remove property.

  • Drop-down lists (LookupField, LookupPickerField).

    • SHIFT-DELETE – clear the value.

  • Standard actions for lookup fields (PickerField, LookupPickerField, SearchPickerField). In addition to these application properties, a shortcut for a particular action can be set by calling its setShortcut() method.

    • CTRL-ALT-L – call the LookupAction. Defined by the cuba.gui.pickerShortcut.lookup.

    • CTRL-ALT-O – call the OpenAction. Defined by the cuba.gui.pickerShortcut.open property.

    • CTRL-ALT-C – call the ClearAction. Defined by the cuba.gui.pickerShortcut.clear property.

    In addition to these shortcuts, lookup fields support action calls with CTRL-ALT-1, CTRL-ALT-2 and so on, depending on the number of actions. If you click CTRL-ALT-1 the first action in the list will be called; clicking CTRL-ALT-2 calls the second action, etc. The CTRL-ALT combination can be replaced with any other combination specified in cuba.gui.pickerShortcut.modifiers property.

  • Filter component.

    • SHIFT-BACKSPACE – open the filter selection popup. Defined by the cuba.gui.filterSelectShortcut property.

    • SHIFT-ENTER – apply the selected filter. Defined by the cuba.gui.filterApplyShortcut property.

3.5.13. URL History and Navigation

CUBA URL History and Navigation feature provides browser history and navigation functionality which is essential for many web applications. This functionality consists of the following parts:

  • History – support for the browser Back button. The Forward button is not supported due to impossibility to reproduce all conditions of opening a screen.

  • Routes and Navigation – registration and handling of routes to the application screens.

  • Routing API – a set of methods that enables reflecting a current state of the screen in the URL.

A fragment is the last part of the URL after the "#" symbol. It is used as a route value.

For example, consider the following URL:

host:port/app/#main/42/orders/edit?id=17

In this URL, the fragment is main/42/orders/edit?id=17 and it consists of the following parts:

  • main – the route to the root screen (Main Window);

  • 42 – a state mark which is used by internals of the navigation mechanism;

  • orders/edit – the nested screen’s route;

  • ?id=17 – the parameters part.

All opened screens map their routes to the current URL. For example, when the user browser screen is open and currently active, the application URL looks as follows:

http://localhost:8080/app/#main/0/users

If a screen doesn’t have registered route, only a state mark is appended to the URL fragment. For example:

http://localhost:8080/app/#main/42

For editor screens, the edited entity id is added to address as a parameter if the screen has a registered route. For example:

http://localhost:8080/app/#main/1/users/edit?id=27zy3tj6f47p2e3m4w58vdca9y

Identifiers of the UUID type are encoded as Base32 Crockford Encoding, all other types are used as is.

When the user is not logged in but some screen route is requested, the redirect parameter is used. Suppose the app/#main/orders route is entered in the address. When the application is loaded and login screen is shown, the address will be changed to: app/#login?redirectTo=orders. After logging in, the screen corresponding to the orders route will be opened.

If the requested route does non exist, the application shows an empty screen with the "Not Found" caption.

The URL History and Navigation feature is enabled by default. The cuba.web.urlHandlingMode application property allows you to disable it using the NONE value, or to revert to the old mechanism of handling the browser Back button using the BACK_ONLY value.

3.5.13.1. Handling URL Changes

The framework automatically reacts to the changes in the application URL: it tries to resolve the requested route and performs history navigation or opens a screen registered for this route.

When a screen is opened by a route with parameters, the framework sends a UrlParamsChangedEvent to the screen controller before the screen is shown. The same is happened when the URL parameters are changed while the screen is open. You can subscribe to this event to handle the initial parameters and their changes. For example, you can load data or hide/show screen UI components depending on the URL parameters.

An example of subscribing to the event in a screen controller:

@Subscribe
protected void onUrlParamsChanged(UrlParamsChangedEvent event) {
    // handle
}

See a complete example of using UrlParamsChangedEvent below.

3.5.13.2. Routing API

This section describes the key concepts of the routing API.

Route Registration

In order to register a route for a screen, add the @Route annotation to the screen controller, for example:

@Route("my-screen")
public class MyScreen extends Screen {
}

The annotation has three parameters:

  • path (or value) is the route itself;

  • parentPrefix is used for routes squashing (see below).

  • root is the boolean property which enables to specify whether a route is registered for the root screen (like Login screen or Main screen). The default value is false.

If you need to define a route for a legacy screen add the route (and optionally routeParentPrefix equivalent to the parentPrefix parameter, rootRoute equivalent to the root parameter) attribute to the screen’s element in the screens.xml file, for example:

<screen id="myScreen" template="..." route="my-screen" />
Route Squashing

This feature is designed to keep the URL clean and readable when opening multiple screens with routes having the same parts.

Suppose that we have browser and editor screens for the Order entity:

@Route("orders")
public class OrderBrowser extends StandardLookup<Order> {
}

@Route("orders/edit")
public class OrderEditor extends StandardEditor<Order> {
}

URL squashing is used to avoid repeating of the orders route in the URL when the editor screen is opened right after browser. Just specify the repeated part in the parentPrefix parameter of the @Route annotation on the editor screen:

@Route("orders")
public class OrderBrowser extends StandardLookup<Order> {
}

@Route("orders/edit", parentPrefix = "orders")
public class OrderEditor extends StandardEditor<Order> {
}

Now when the editor is opened in the same tab as the browser, the resulting address will be like app/#main/0/orders/edit?id=…​

Mapping of UI State to URL

The UrlRouting bean allows you to change the current application URL according to the current screen and some parameters. It has the following methods:

  • pushState() – changes the address and pushes new browser history entry;

  • replaceState() – replaces the address without adding new browser history entry;

  • getState() – returns a current state as NavigationState object.

The pushState()/replaceState() methods accept the current screen controller and optional map of parameters.

See an example of using UrlRouting in the section below.

Navigation Filter

The navigation filters mechanism allows you to prevent transition to some routes.

A navigation filter is a managed bean that implements the NavigationFilter interface. The @Order annotation can be used to configure the order of invocation of all navigation filters. The NavigationFilter.HIGHEST_PLATFORM_PRECEDENCE and NavigationFilter.LOWEST_PLATFORM_PRECEDENCE constants define the range which is used by filters defined in the framework.

The NavigationFilter interface has the allowed() method which accepts two arguments: current navigation state fromState and requested navigation state toState. The method returns AccessCheckResult instance and checks whether the transition from the current navigation state to the requested navigation state is allowed.

CubaLoginScreenFilter is an example of navigation filter. It is designed for checking whether the current session is authenticated to prevent navigation to the login screen when the user is logged in already:

@Component
@Order(NavigationFilter.LOWEST_PLATFORM_PRECEDENCE)
public class CubaLoginScreenFilter implements NavigationFilter {
    @Inject
    protected Messages messages;

    @Override
    public AccessCheckResult allowed(NavigationState fromState, NavigationState toState) {
        if (!"login".equals(toState.getRoot())) {
            return AccessCheckResult.allowed();
        }
        boolean authenticated = App.getInstance().getConnection().isAuthenticated();
        return authenticated
                ? AccessCheckResult.rejected(messages.getMainMessage("navigation.unableToGoToLogin"))
                : AccessCheckResult.allowed();
    }
}
3.5.13.3. Using URL History and Navigation API

This section contains examples of using the URL History and Navigation API.

Suppose we have a Task entity and TaskInfo screen with an information about a selected task.

The TaskInfo screen controller contains the @Route annotation to specify the route to the screen:

package com.company.demo.web.navigation;

import com.haulmont.cuba.gui.Route;
import com.haulmont.cuba.gui.screen.Screen;
import com.haulmont.cuba.gui.screen.UiController;
import com.haulmont.cuba.gui.screen.UiDescriptor;

@Route("task-info")
@UiController("demo_TaskInfoScreen")
@UiDescriptor("task-info.xml")
public class TaskInfoScreen extends Screen {
}

As a result, a user can open the screen by entering http://localhost:8080/app/#main/task-info in the address bar:

url screen by route

When the screen is open, the address contains also a state mark.

Mapping State to URL

Suppose that the TaskInfo screen shows information about a single task at a time, and it has controls to switch the selected tasks. You may want to reflect the currently viewed task in the URL to be able to copy the URL and later open the screen for this particular task just by pasting the URL to the address bar.

The following code implements mapping of the selected task to the URL:

package com.company.demo.web.navigation;

import com.company.demo.entity.Task;
import com.google.common.collect.ImmutableMap;
import com.haulmont.cuba.gui.Route;
import com.haulmont.cuba.gui.UrlRouting;
import com.haulmont.cuba.gui.components.Button;
import com.haulmont.cuba.gui.components.LookupField;
import com.haulmont.cuba.gui.screen.*;
import com.haulmont.cuba.web.sys.navigation.UrlIdSerializer;

import javax.inject.Inject;

@Route("task-info")
@UiController("demo_TaskInfoScreen")
@UiDescriptor("task-info.xml")
@LoadDataBeforeShow
public class TaskInfoScreen extends Screen {

    @Inject
    private LookupField<Task> taskField;

    @Inject
    private UrlRouting urlRouting;

    @Subscribe("selectBtn")
    protected void onSelectBtnClick(Button.ClickEvent event) {
        Task task = taskField.getValue(); (1)
        if (task == null) {
            urlRouting.replaceState(this); (2)
            return;
        }
        String serializedTaskId = UrlIdSerializer.serializeId(task.getId()); (3)

        urlRouting.replaceState(this, ImmutableMap.of("task_id", serializedTaskId)); (4)
    }
}
1 - get the current task from LookupField
2 - remove URL parameters if no task is selected
3 - serialize task’s id with the UrlIdSerializer helper
4 - replace the current URL state with the new one containing serialized task id as a parameter.

As a result, the application URL is changed when the user selects a task and clicks the Select Task button:

url reflection state
UrlParamsChangedEvent

Now let’s implement the last requirement: when a user enters the URL with the route and the task_id parameter, the application must show the screen with the corresponding task selected. Below is the complete screen controller code.

package com.company.demo.web.navigation;

import com.company.demo.entity.Task;
import com.google.common.collect.ImmutableMap;
import com.haulmont.cuba.core.global.DataManager;
import com.haulmont.cuba.gui.Route;
import com.haulmont.cuba.gui.UrlRouting;
import com.haulmont.cuba.gui.components.Button;
import com.haulmont.cuba.gui.components.LookupField;
import com.haulmont.cuba.gui.navigation.UrlParamsChangedEvent;
import com.haulmont.cuba.gui.screen.*;
import com.haulmont.cuba.web.sys.navigation.UrlIdSerializer;

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

@Route("task-info")
@UiController("demo_TaskInfoScreen")
@UiDescriptor("task-info.xml")
@LoadDataBeforeShow
public class TaskInfoScreen extends Screen {

    @Inject
    private LookupField<Task> taskField;

    @Inject
    private UrlRouting urlRouting;

    @Inject
    private DataManager dataManager;

    @Subscribe
    protected void onUrlParamsChanged(UrlParamsChangedEvent event) {
        String serializedTaskId = event.getParams().get("task_id"); (1)

        UUID taskId = (UUID) UrlIdSerializer.deserializeId(UUID.class, serializedTaskId); (2)

        taskField.setValue(dataManager.load(Task.class).id(taskId).one()); (3)
    }

    @Subscribe("selectBtn")
    protected void onSelectBtnClick(Button.ClickEvent event) {
        Task task = taskField.getValue();
        if (task == null) {
            urlRouting.replaceState(this);
            return;
        }
        String serializedTaskId = UrlIdSerializer.serializeId(task.getId());

        urlRouting.replaceState(this, ImmutableMap.of("task_id", serializedTaskId));
    }
}
1 - get the parameter value from UrlParamsChangedEvent
2 - deserialize the task id
3 - load the task instance and set it to the field
3.5.13.4. URL Routes Generator

Sometimes, it is necessary to get a proper URL of some application screen that can be sent via email or shown to the user. The simplest way to generate it is by using URL Routes Generator.

URL Routes Generator provides API for generating links to an entity editor screen or a screen defined by its id or class. The link can also contain URL parameters that enable to reflect inner screen state to URL to use it later.

The getRouteGenerator() method of UrlRouting bean allows you to get an instance of RouteGenerator. RouteGenerator has the following methods:

  • getRoute(String screenId) – returns a route for a screen with given screenId, for example:

    String route = urlRouting.getRouteGenerator().getRoute("demo_Customer.browse");

    The resulting URL will be route = "http://host:port/context/#main/customers"

  • getRoute(Class<? extends Screen> screenClass) – generates a route for screen with the given screenClass, for example:

    String route = urlRouting.getRouteGenerator().getRoute(CustomerBrowse.class);

    The resulting URL will be route = "http://host:port/context/#main/customers"

  • getEditorRoute(Entity entity) – generates a route to a default editor screen of the given entity, for example:

    Customer сustomer = customersTable.getSingleSelected();
    
    String route = urlRouting.getRouteGenerator().getEditorRoute(сustomer);

    The resulting URL will be route = "http://localhost:8080/app/#main/customers/edit?id=5jqtc3pwzx6g6mq1vv5gkyjn0s"

  • getEditorRoute(Entity entity, Class<? extends Screen> screenClass) – generates a route for editor with the given screenClass and entity.

  • getRoute(Class<? extends Screen> screenClass, Map<String, String> urlParams) – generates a route for screen with the given screenClass and urlParams.

URL Routes Generator Example

Suppose that we have a Customer entity with standard screens that have registered routes. Let’s add a button to the browser screen that generates a link to the editor of the selected entity:

@Inject
private UrlRouting urlRouting;

@Inject
private GroupTable<Customer> customersTable;

@Inject
private Dialogs dialogs;

@Subscribe("getLinkButton")
public void onGetLinkButtonClick(Button.ClickEvent event) {
    Customer selectedCustomer = customersTable.getSingleSelected();
    if (selectedCustomer != null) {
        String routeToSelectedRole = urlRouting.getRouteGenerator()
                .getEditorRoute(selectedCustomer);

        dialogs.createMessageDialog()
                .withCaption("Generated route")
                .withMessage(routeToSelectedRole)
                .withWidth("710")
                .show();
    }
}

The resulting route looks like this:

url generate route

3.5.14. Composite Components

A composite component is a component consisting of other components. Like screen fragments, composite components allow you to reuse some presentation layout and logic. We recommend using composite components in the following cases:

  • The component functionality can be implemented as a combination of existing Generic UI components. If you need some non-standard features, create a custom component by wrapping a Vaadin component or JavaScript library, or use Generic JavaScriptComponent.

  • The component is relatively simple and does not load or save data itself. Otherwise, consider creating a screen fragment.

The class of a composite component must extend the CompositeComponent base class. A composite component must have a single component at the root of the inner components tree. The root component can be obtained using the CompositeComponent.getComposition() method.

Inner components are usually created declaratively in an XML descriptor. In this case, the component class should have the @CompositeDescriptor annotation which specifies the path to the descriptor file. If the annotation value does not start with /, the file is loaded from the component’s class package.

Alternatively, the inner components tree can be created programmatically in a CreateEvent listener.

CreateEvent is sent by the framework when it finishes the initialization of the component. At this moment, if an XML descriptor is used by the component, it is loaded, and getComposition() method returns the root component. This event can be used for any additional initialization of the component or for creating the inner components without XML.

Below we demonstrate the creation of Stepper component which is designed to edit integer values in the input field and by clicking on up/down buttons located next to the field.

Let’s assume the project has com/company/demo base package.

Component layout descriptor

Create an XML descriptor with the component layout in the com/company/demo/web/components/stepper/stepper-component.xml file of the web module:

<composite xmlns="http://schemas.haulmont.com/cuba/screen/composite.xsd"> (1)
    <hbox id="rootBox" width="100%" expand="valueField"> (2)
        <textField id="valueField"/> (3)
        <button id="upBtn"
                icon="font-icon:CHEVRON_UP"/>
        <button id="downBtn"
                icon="font-icon:CHEVRON_DOWN"/>
    </hbox>
</composite>
1 - XSD defines the content of the component descriptor
2 - a single root component
3 - any number of nested components
Component implementation class

Create the component implementation class in the same package:

package com.company.demo.web.components.stepper;

import com.haulmont.bali.events.Subscription;
import com.haulmont.cuba.gui.components.*;
import com.haulmont.cuba.gui.components.data.ValueSource;
import com.haulmont.cuba.web.gui.components.*;

import java.util.Collection;
import java.util.function.Consumer;

@CompositeDescriptor("stepper-component.xml") (1)
public class StepperField
        extends CompositeComponent<HBoxLayout> (2)
        implements Field<Integer>, (3)
                    CompositeWithCaption, (4)
                    CompositeWithHtmlCaption,
                    CompositeWithHtmlDescription,
                    CompositeWithIcon,
                    CompositeWithContextHelp {

    public static final String NAME = "stepperField"; (5)

    private TextField<Integer> valueField; (6)
    private Button upBtn;
    private Button downBtn;

    private int step = 1; (7)

    public StepperField() {
        addCreateListener(this::onCreate); (8)
    }

    private void onCreate(CreateEvent createEvent) {
        valueField = getInnerComponent("valueField");
        upBtn = getInnerComponent("upBtn");
        downBtn = getInnerComponent("downBtn");

        upBtn.addClickListener(clickEvent -> updateValue(step));
        downBtn.addClickListener(clickEvent -> updateValue(-step));
    }

    private void updateValue(int delta) {
        Integer value = getValue();
        setValue(value != null ? value + delta : delta);
    }

    public int getStep() {
        return step;
    }

    public void setStep(int step) {
        this.step = step;
    }

    @Override
    public boolean isRequired() { (9)
        return valueField.isRequired();
    }

    @Override
    public void setRequired(boolean required) {
        valueField.setRequired(required);
        getComposition().setRequiredIndicatorVisible(required);
    }

    @Override
    public String getRequiredMessage() {
        return valueField.getRequiredMessage();
    }

    @Override
    public void setRequiredMessage(String msg) {
        valueField.setRequiredMessage(msg);
    }

    @Override
    public void addValidator(Consumer<? super Integer> validator) {
        valueField.addValidator(validator);
    }

    @Override
    public void removeValidator(Consumer<Integer> validator) {
        valueField.removeValidator(validator);
    }

    @Override
    public Collection<Consumer<Integer>> getValidators() {
        return valueField.getValidators();
    }

    @Override
    public boolean isEditable() {
        return valueField.isEditable();
    }

    @Override
    public void setEditable(boolean editable) {
        valueField.setEditable(editable);
        upBtn.setEnabled(editable);
        downBtn.setEnabled(editable);
    }

    @Override
    public Integer getValue() {
        return valueField.getValue();
    }

    @Override
    public void setValue(Integer value) {
        valueField.setValue(value);
    }

    @Override
    public Subscription addValueChangeListener(Consumer<ValueChangeEvent<Integer>> listener) {
        return valueField.addValueChangeListener(listener);
    }

    @Override
    public void removeValueChangeListener(Consumer<ValueChangeEvent<Integer>> listener) {
        valueField.removeValueChangeListener(listener);
    }

    @Override
    public boolean isValid() {
        return valueField.isValid();
    }

    @Override
    public void validate() throws ValidationException {
        valueField.validate();
    }

    @Override
    public void setValueSource(ValueSource<Integer> valueSource) {
        valueField.setValueSource(valueSource);
        getComposition().setRequiredIndicatorVisible(valueField.isRequired());
    }

    @Override
    public ValueSource<Integer> getValueSource() {
        return valueField.getValueSource();
    }
}
1 - the @CompositeDescriptor annotation specifies the path to the component layout descriptor which is located in the class package.
2 - the component class extends CompositeComponent parameterized by the type of the root component.
3 - our component implements the Field<Integer> interface because it is designed to display and edit an integer value.
4 - a set of interfaces with default methods to implement standard Generic UI component functionality.
5 - name of the component which is used to register the component in ui-component.xml file to be recognized by the framework.
6 - fields containing references to inner components.
7 - component’s property which defines the value of a single click to up/down buttons. It has public getter/setter methods and can be assigned in screen XML.
8 - component initialization is done in the CreateEvent listener.
Component loader

Create the component loader which is needed to initialize the component when it is used in screen XML descriptors:

package com.company.demo.web.components.stepper;

import com.google.common.base.Strings;
import com.haulmont.cuba.gui.xml.layout.loaders.AbstractFieldLoader;

public class StepperFieldLoader extends AbstractFieldLoader<StepperField> { (1)

    @Override
    public void createComponent() {
        resultComponent = factory.create(StepperField.NAME); (2)
        loadId(resultComponent, element);
    }

    @Override
    public void loadComponent() {
        super.loadComponent();
        String incrementStr = element.attributeValue("step"); (3)
        if (!Strings.isNullOrEmpty(incrementStr)) {
            resultComponent.setStep(Integer.parseInt(incrementStr));
        }
    }
}
1 - loader class must extend AbstractComponentLoader parameterized by the class of the component. As our component implements Field, use more specific AbstractFieldLoader base class.
2 - create the component by its name.
3 - load the step property value from XML if it is specified.
Registration of the component

In order to register the component and its loader with the framework, create the com/company/demo/ui-component.xml file of the web module:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<components xmlns="http://schemas.haulmont.com/cuba/components.xsd">
    <component>
        <name>stepperField</name>
        <componentLoader>com.company.demo.web.components.stepper.StepperFieldLoader</componentLoader>
        <class>com.company.demo.web.components.stepper.StepperField</class>
    </component>
</components>

Add the following property to com/company/demo/web-app.properties:

cuba.web.componentsConfig = +com/company/demo/ui-component.xml

Now the framework will recognize the new component in XML descriptors of application screens.

Component XSD

XSD is required to use the component in screen XML descriptors. Define it in the com/company/demo/ui-component.xsd file of the web module:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<xs:schema xmlns="http://schemas.company.com/demo/0.1/ui-component.xsd"
           attributeFormDefault="unqualified"
           elementFormDefault="qualified"
           targetNamespace="http://schemas.company.com/demo/0.1/ui-component.xsd"
           xmlns:xs="http://www.w3.org/2001/XMLSchema"
           xmlns:layout="http://schemas.haulmont.com/cuba/screen/layout.xsd">

    <xs:element name="stepperField">
        <xs:complexType>
            <xs:complexContent>
                <xs:extension base="layout:baseFieldComponent"> (1)
                    <xs:attribute name="step" type="xs:integer"/> (2)
                </xs:extension>
            </xs:complexContent>
        </xs:complexType>
    </xs:element>
</xs:schema>
1 - inherit all base field properties.
2 - define an attribute for the step property.
Usage of the component

The following example shows how the component can be used in a screen:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd"
        xmlns:app="http://schemas.company.com/demo/0.1/ui-component.xsd" (1)
        caption="msg://caption"
        messagesPack="com.company.demo.web.components.sample">
    <data>
        <instance id="fooDc" class="com.company.demo.entity.Foo" view="_local">
            <loader/>
        </instance>
    </data>
    <layout>
        <form id="form" dataContainer="fooDc">
            <column width="250px">
                <textField id="nameField" property="name"/>
                <app:stepperField id="ageField" property="limit" step="10"/> (2)
            </column>
        </form>
    </layout>
</window>
1 - the namespace referencing the component’s XSD.
2 - the composite component connected to the limit attribute of an entity.
Custom styling

Now let’s apply some custom styles to improve the component look.

First, change the root component to CssLayout and assign style names to the inner components. Besides custom styles defined in the project (see below), the following predefined styles are used: v-component-group, icon-only.

<composite xmlns="http://schemas.haulmont.com/cuba/screen/composite.xsd">
    <cssLayout id="rootBox" width="100%" stylename="v-component-group stepper-field">
        <textField id="valueField"/>
        <button id="upBtn"
                icon="font-icon:CHEVRON_UP"
                stylename="stepper-btn icon-only"/>
        <button id="downBtn"
                icon="font-icon:CHEVRON_DOWN"
                stylename="stepper-btn icon-only"/>
    </cssLayout>
</composite>

Change the component class accordingly:

@CompositeDescriptor("stepper-component.xml")
public class StepperField
        extends CompositeComponent<CssLayout>
        implements ...

Generate theme extension (see here how to do it in Studio) and add the following code to the modules/web/themes/hover/com.company.demo/hover-ext.scss file:

@mixin com_company_demo-hover-ext {

  .stepper-field {
    display: flex;

    .stepper-btn {
      width: $v-unit-size;
      min-width: $v-unit-size;
    }
  }
}

Restart the application server and open the screen. The form with our composite Stepper component should look as follows:

stepper final

3.5.15. Pluggable Component Factories

The pluggable component factories mechanism extends the standard component creation procedure and allows you to create different edit fields in Form, Table and DataGrid. It means that application components or your project itself can provide custom strategies that will create non-standard components and/or support custom data types.

An entry point to the mechanism is the UiComponentsGenerator.generate(ComponentGenerationContext) method. It works as follows:

  • Tries to find ComponentGenerationStrategy implementations. If at least one strategy exists, then:

    • Iterates over strategies according to the org.springframework.core.Ordered interface.

    • Returns the first created not null component.

ComponentGenerationStrategy implementations are used to create UI components. A project can contain any number of such strategies.

ComponentGenerationContext is a class which stores the following information that can be used when creating a component:

  • metaClass - defines the entity for which the component is created.

  • property - defines the entity attribute for which the component is created.

  • datasource - a datasource.

  • optionsDatasource - a datasource that can be used to show options.

  • valueSource - a value source that can be used to create the component.

  • options - an options object that can be used to show options.

  • xmlDescriptor - an XML descriptor which contains additional information, in case the component is declared in an XML descriptor.

  • componentClass - a component class for which a component is created, e.g. Form, Table, DataGrid.

There are two built-in component strategies:

  • DefaultComponentGenerationStrategy - used to create a component according to the given ComponentGenerationContext object. Has the order value ComponentGenerationStrategy.LOWEST_PLATFORM_PRECEDENCE (1000).

  • DataGridEditorComponentGenerationStrategy - used to create a component according to the given ComponentGenerationContext object for a DataGrid Editor. Has the order value ComponentGenerationStrategy.HIGHEST_PLATFORM_PRECEDENCE + 30 (130).

The sample below shows how to replace the default Form field generation for a certain attribute of a specific entity.

import com.company.sales.entity.Order;
import com.haulmont.chile.core.model.MetaClass;
import com.haulmont.cuba.core.global.Metadata;
import com.haulmont.cuba.gui.UiComponents;
import com.haulmont.cuba.gui.components.*;
import com.haulmont.cuba.gui.components.data.ValueSource;
import org.springframework.core.Ordered;

import javax.annotation.Nullable;
import javax.inject.Inject;
import java.sql.Date;

@org.springframework.stereotype.Component(SalesComponentGenerationStrategy.NAME)
public class SalesComponentGenerationStrategy implements ComponentGenerationStrategy, Ordered {

    public static final String NAME = "sales_SalesComponentGenerationStrategy";

    @Inject
    private UiComponents uiComponents;

    @Inject
    private Metadata metadata;

    @Nullable
    @Override
    public Component createComponent(ComponentGenerationContext context) {
        String property = context.getProperty();
        MetaClass orderMetaClass = metadata.getClassNN(Order.class);

        // Check the specific field of the Order entity
        // and that the component is created for the Form component
        if (orderMetaClass.equals(context.getMetaClass())
                && "date".equals(property)
                && context.getComponentClass() != null
                && Form.class.isAssignableFrom(context.getComponentClass())) {
            DatePicker<Date> datePicker = uiComponents.create(DatePicker.TYPE_DATE);

            ValueSource valueSource = context.getValueSource();
            if (valueSource != null) {
                //noinspection unchecked
                datePicker.setValueSource(valueSource);
            }

            return datePicker;
        }

        return null;
    }

    @Override
    public int getOrder() {
        return 50;
    }
}

The sample below shows how to define a ComponentGenerationStrategy for a specific datatype.

import com.company.colordatatype.datatypes.ColorDatatype;
import com.haulmont.chile.core.datatypes.Datatype;
import com.haulmont.chile.core.model.MetaClass;
import com.haulmont.chile.core.model.MetaPropertyPath;
import com.haulmont.chile.core.model.Range;
import com.haulmont.cuba.core.app.dynamicattributes.DynamicAttributesUtils;
import com.haulmont.cuba.gui.UiComponents;
import com.haulmont.cuba.gui.components.ColorPicker;
import com.haulmont.cuba.gui.components.Component;
import com.haulmont.cuba.gui.components.ComponentGenerationContext;
import com.haulmont.cuba.gui.components.ComponentGenerationStrategy;
import com.haulmont.cuba.gui.components.data.ValueSource;
import org.springframework.core.annotation.Order;

import javax.annotation.Nullable;
import javax.inject.Inject;

@Order(100)
@org.springframework.stereotype.Component(ColorComponentGenerationStrategy.NAME)
public class ColorComponentGenerationStrategy implements ComponentGenerationStrategy {

    public static final String NAME = "colordatatype_ColorComponentGenerationStrategy";

    @Inject
    private UiComponents uiComponents;

    @Nullable
    @Override
    public Component createComponent(ComponentGenerationContext context) {
        String property = context.getProperty();
        MetaPropertyPath mpp = resolveMetaPropertyPath(context.getMetaClass(), property);

        if (mpp != null) {
            Range mppRange = mpp.getRange();
            if (mppRange.isDatatype()
                    && ((Datatype) mppRange.asDatatype()) instanceof ColorDatatype) {
                ColorPicker colorPicker = uiComponents.create(ColorPicker.class);
                colorPicker.setDefaultCaptionEnabled(true);

                ValueSource valueSource = context.getValueSource();
                if (valueSource != null) {
                    //noinspection unchecked
                    colorPicker.setValueSource(valueSource);
                }

                return colorPicker;
            }
        }

        return null;
    }

    protected MetaPropertyPath resolveMetaPropertyPath(MetaClass metaClass, String property) {
        MetaPropertyPath mpp = metaClass.getPropertyPath(property);

        if (mpp == null && DynamicAttributesUtils.isDynamicAttribute(property)) {
            mpp = DynamicAttributesUtils.getMetaPropertyPath(metaClass, property);
        }

        return mpp;
    }
}

3.5.16. Working with Vaadin Components

In order to work directly with Vaadin components that implement interfaces of the visual components library in Web Client, use the following methods of the Component interface:

  • unwrap() – retrieves an underlying Vaadin component for a given CUBA component.

  • unwrapComposition() - retrieves a Vaadin component that is the outmost external container in the implementation of a given CUBA component. For simple components, such as Button, this method returns the same object as unwrap() - com.vaadin.ui.Button. For complex components, such as Table, unwrap() will return the corresponding object - com.vaadin.ui.Table, while unwrapComposition() will return com.vaadin.ui.VerticalLayout, which contains the table together with ButtonsPanel and RowsCount defined with it.

The methods accept a class of the underlying component to be returned, for example:

com.vaadin.ui.TextField vTextField = textField.unwrap(com.vaadin.ui.TextField.class);

You can also use the unwrap() and getComposition() static methods of the WebComponentsHelper class, passing a CUBA component into them.

Please note that if a screen is located in the project’s gui module, you can only work with generalized interfaces of CUBA components. In order to use unwrap() you should either put the entire screen into the web module, or use the controller companions mechanism.

3.5.17. Custom Visual Components

This section contains an overview of different ways of creating custom web UI components in CUBA applications. The practical tutorial of using these approaches is located in the Creating Custom Visual Components section.

Before creating a component using a low-level technology, consider composite components based on existing Generic UI components.

A new component can be created with the following technologies:

  1. On the basis of a Vaadin add-on.

    This is the simplest method. The following steps are required to use an add-on in your application:

    • Add a dependency to the add-on artifact to build.gradle.

    • Create the web-toolkit module in your project. This module contains a GWT widgetset file and allows you to create client-side parts of visual components.

    • Include the add-on widgetset to the widgetset of your project.

    • If the component’s look does not fit the application theme, create a theme extension and define some CSS for the new component.

    See an example in the Using a Third-party Vaadin Component section.

  2. As a wrapper of a JavaScript library.

    This method is recommended if you already have a pure JavaScript component that does what you need. To use it in your application, you need to do the following:

    • Create a server-side Vaadin component in the web module. A server component defines an API for server code, access methods, event listeners, etc. The server component must extend the AbstractJavaScriptComponent class. Note that the web-toolkit module with a widgetset is not required when integrating a JavaScript component.

    • Create a JavaScript connector. A connector is a function that initializes the JavaScript component and is responsible for interaction between JavaScript and the server-side code.

    • Create a state class. Its public fields define what data are sent from the server to the client. The class must extend JavaScriptComponentState.

    See an example in the Using a JavaScript library section.

  3. As a resource from WebJar. See the section below for details.

  4. As a new GWT component.

    This is the recommended method of creating completely new visual components. The following steps are required to create and use a GWT component in your application:

    • Create the web-toolkit module.

    • Create a client-side GWT widget class.

    • Create a server-side Vaadin component.

    • Create a component state class that defines what data are sent between the client and the server.

    • Create a connector class that links the client code with the server component.

    • Create an RPC interface that defines a server API that is invoked from the client.

    See an example in the Creating a GWT component section.

There are three levels of integration of a new component into the platform.

  • On the first level, the new component becomes available as a native Vaadin component. An application developer can use this component in screen controllers directly: create a new instance and add it to an unwrapped container. All methods of creating new components described above give you a component on this level of integration.

  • On the second level, the new component is integrated into CUBA Generic UI. In this case, from an application developer perspective, it looks the same as a standard component from the visual components library. The developer can define the component in a screen XML descriptor or create it through UiComponents in a controller. See an example in the Integrating a Vaadin Component into the Generic UI section.

  • On the third level, the new component is available on the Studio components palette and can be used in the WYSIWYG layout editor. See an example in the Support for Custom Visual Components in CUBA Studio section.

3.5.17.1. Using WebJars

This method allows you to use various JS libraries packaged into JAR files and deployed on Maven Central. The following steps are required to use a component from a WebJar in your application:

  • Add dependency to the compile method of web module:

    compile 'org.webjars.bower:jrcarousel:1.0.0'
  • Create the web-toolkit module.

  • Create a client-side GWT widget class and implement the native JSNI method for creating the component.

  • Create a server-side component class with @WebJarResource annotation.

    This annotation should be used only with ClientConnector inheritors (which are classes of UI components from the web-toolkit module usually).

    The value of the @WebJarResource annotation, or the resource definition, should follow one of the two possible templates:

    1. <webjar_name>:<sub_path>, for example:

      @WebJarResource("pivottable:plugins/c3/c3.min.css")
    2. <webjar_name>/<resource_version>/<webjar_resource>, for example:

      @WebJarResource("jquery-ui/1.12.1/jquery-ui.min.js")

    The annotation value can have one or more (String array) WebJar resource String definitions:

    @WebJarResource({
            "jquery-ui:jquery-ui.min.js",
            "jquery-fileupload:jquery-fileupload.min.js",
            "jquery-fileupload:jquery-fileupload.min.js"
    })
    public class CubaFileUpload extends CubaAbstractUploadComponent {
        ...
    }

    Specifying the WebJar version is not required, as due to Maven version resolution strategy the WebJar with the higher version will be used automatically.

    Optionally, you can specify a directory inside VAADIN/webjars/ from which the static resources will be served. Thus you can override WebJar resources by placing new versions of resources in this directory. To set the path, use the overridePath property of the @WebJarResource annotation, for example:

    @WebJarResource(value = "pivottable:plugins/c3/c3.min.css", overridePath = "pivottable")
  • Add new component to the screen.

3.5.17.2. Generic JavaScriptComponent

JavaScriptComponent is a simple UI component that can work with any JavaScript wrapper without Vaadin component implementation. Thus, you can easily integrate any pure JavaScript component in your CUBA-based application.

The component can be defined declaratively in an XML descriptor of the screen, so that you can configure dynamic properties and JavaScript dependencies in XML.

XML-name of the component: jsComponent.

Defining dependencies

You can define a list of dependencies (JavaScript, CSS) for the component. A dependency can be obtained from the following sources:

  • WebJar resource - starts with webjar://

  • File placed within VAADIN directory - starts with vaadin://

  • Web resource - starts with http:// or https://

If the type of dependency cannot be inferred from the extension, specify the type explicitly in the type XML attribute or by passing DependencyType enum value to the addDependency() method.

Example of defining dependencies in XML:

<jsComponent ...>
    <dependencies>
        <dependency path="webjar://leaflet.js"/>
        <dependency path="http://code.jquery.com/jquery-3.4.1.min.js"/>
        <dependency path="http://api.map.baidu.com/getscript?v=2.0"
                    type="JAVASCRIPT"/>
    </dependencies>
</jsComponent>

Example of adding dependencies programmatically:

jsComponent.addDependencies(
        "webjar://leaflet.js",
        "http://code.jquery.com/jquery-3.4.1.min.js"
);
jsComponent.addDependency(
        "http://api.map.baidu.com/getscript?v=2.0", DependencyType.JAVASCRIPT
);
Defining initialization function

The component requires an initialization function. This function’s name that will be used to find an entry point for the JavaScript component connector (see below).

The initialization function name must be unique within a window.

The function name can be passed to the component using the setInitFunctionName() method:

jsComponent.setInitFunctionName("com_company_demo_web_screens_Sandbox");
Defining JavaScript connector

To use JavaScriptComponent wrapper for a library, you should define a JavaScript connector - a function that initializes the JavaScript component and handles communication between the server-side and the JavaScript code.

The following methods are available from the connector function:

  • this.getElement() returns the HTML DOM element of the component.

  • this.getState() returns a shared state object with the current state as synchronized from the server-side.

Component features

The JavaScriptComponent component has the following features that let you:

  • Set a state object that can be used in the client-side JavaScript connector and is accessible from the data field of the component’s state, for example:

    MyState state = new MyState();
    state.minValue = 0;
    state.maxValue = 100;
    jsComponent.setState(state);
  • Register a function that can be called from the JavaScript using the provided name, for example:

    jsComponent.addFunction("valueChanged", callbackEvent -> {
        JsonArray arguments = callbackEvent.getArguments();
    
        notifications.create()
                .withCaption(StringUtils.join(arguments, ", "))
                .show();
    });
    this.valueChanged(values);
  • Invoke a named function that the connector JavaScript has added to the JavaScript connector wrapper object.

    jsComponent.callFunction("showNotification ");
    this.showNotification = function () {
            alert("TEST");
    };
JavaScriptComponent usage example

This section describes how to integrate a third-party JavaScript library to a CUBA-based application taking Quill Rich Text Editor from https://quilljs.com/ as an example. To use Quill in your project, you should follow the steps below.

  1. Add the following dependency to the web module:

    compile('org.webjars.npm:quill:1.3.6')
  2. Create a quill-connector.js file in the web/VAADIN/quill directory on the web module.

  3. In this file, add the connector implementation:

    com_company_demo_web_screens_Sandbox = function () {
        var connector = this;
        var element = connector.getElement();
        element.innerHTML = "<div id=\"editor\">" +
            "<p>Hello World!</p>" +
            "<p>Some initial <strong>bold</strong> text</p>" +
            "<p><br></p>" +
            "</div>";
    
        connector.onStateChange = function () {
            var state = connector.getState();
            var data = state.data;
    
            var quill = new Quill('#editor', data.options);
    
            // Subscribe on textChange event
            quill.on('text-change', function (delta, oldDelta, source) {
                if (source === 'user') {
                    connector.valueChanged(quill.getText(), quill.getContents());
                }
            });
        }
    };
  4. Create a screen with the following jsComponent definition:

    <jsComponent id="quill"
                 initFunctionName="com_company_demo_web_screens_Sandbox"
                 height="200px"
                 width="400">
        <dependencies>
            <dependency path="webjar://quill:dist/quill.js"/>
            <dependency path="webjar://quill:dist/quill.snow.css"/>
            <dependency path="vaadin://quill/quill-connector.js"/>
        </dependencies>
    </jsComponent>
  5. Add the following screen controller implementation:

    @UiController("demo_Sandbox")
    @UiDescriptor("sandbox.xml")
    public class Sandbox extends Screen {
        @Inject
        private JavaScriptComponent quill;
    
        @Inject
        private Notifications notifications;
    
        @Subscribe
        protected void onInit(InitEvent event) {
            QuillState state = new QuillState();
            state.options = ParamsMap.of("theme", "snow",
                    "placeholder", "Compose an epic...");
    
            quill.setState(state);
    
            quill.addFunction("valueChanged", javaScriptCallbackEvent -> {
                String value = javaScriptCallbackEvent.getArguments().getString(0);
                notifications.create()
                        .withCaption(value)
                        .withPosition(Notifications.Position.BOTTOM_RIGHT)
                        .show();
            });
        }
    
        class QuillState {
            public Map<String, Object> options;
        }
    }

As a result, the Quill Rich Text Editor is available on the screen:

jsComponent example

Another example of custom JavaScript component integration see at Using a JavaScript library.

3.5.17.3. 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 data containers.

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.

3.5.17.3.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. A convenient way to create it is to use CUBA Studio: in the main menu, click CUBA > Advanced > Manage modules > Create 'web-toolkit' Module.

Then add add-on dependencies:

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

    configure(webModule) {
        ...
        dependencies {
            ...
            compile("org.vaadin.addons:stepper:2.4.0")
        }
  2. In AppWidgetSet.gwt.xml file of the web-toolkit module, indicate that the project’s widgetset inherits from the add-on widgetset:

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

    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 Module field is set to Module: 'app-web_main' (this field is shown only if the gui module is added to the project). Screens that use Vaadin components directly must be placed in the web module.

    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.

    Replace the score field of the form component of the customer-edit.xml screen with a hBox that will be used as a container for a Vaadin component.

    <?xml version="1.0" encoding="UTF-8" standalone="no"?>
    <window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd"
            caption="msg://editorCaption"
            focusComponent="form"
            messagesPack="com.company.demo.web.customer">
        <data>
            <instance id="customerDc"
                      class="com.company.demo.entity.Customer"
                      view="_local">
                <loader/>
            </instance>
        </data>
        <dialogMode height="600"
                    width="800"/>
        <layout expand="editActions" spacing="true">
            <form id="form" dataContainer="customerDc">
                <column width="250px">
                    <textField id="nameField" property="name"/>
                    <!-- A box that will be used as a container for a Vaadin component -->
                    <hbox id="scoreBox"
                          caption="msg://com.company.demo.entity/Customer.score"
                          height="100%"
                          width="100%"/>
                </column>
            </form>
            <hbox id="editActions" spacing="true">
                <button action="windowCommitAndClose"/>
                <button action="windowClose"/>
            </hbox>
        </layout>
    </window>

    Add the following code to the CustomerEdit.java controller:

    package com.company.demo.web.customer;
    
    import com.company.demo.entity.Customer;
    import com.haulmont.cuba.gui.components.HBoxLayout;
    import com.haulmont.cuba.gui.screen.*;
    import com.vaadin.ui.Layout;
    import org.vaadin.risto.stepper.IntStepper;
    
    import javax.inject.Inject;
    
    @UiController("demo_Customer.edit")
    @UiDescriptor("customer-edit.xml")
    @EditedEntityContainer("customerDc")
    @LoadDataBeforeShow
    public class CustomerEdit extends StandardEditor<Customer> {
        @Inject
        private HBoxLayout scoreBox;
    
        private IntStepper stepper = new IntStepper();
    
        @Subscribe
        protected void onInit(InitEvent event) {
            scoreBox.unwrap(Layout.class)
                    .addComponent(stepper);
    
            stepper.setSizeFull();
            stepper.addValueChangeListener(valueChangeEvent ->
                    getEditedEntity().setScore(valueChangeEvent.getValue()));
        }
    
        @Subscribe
        protected void onInitEntity(InitEntityEvent<Customer> event) {
            event.getEntity().setScore(0);
        }
    
        @Subscribe
        protected void onBeforeShow(BeforeShowEvent event) {
            stepper.setValue(getEditedEntity().getScore());
        }
    }

    The onInit() method initializes a stepper component instance, retrieves a link to the Vaadin container using the unwrap method, and adds the new component to it.

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

  • To adapt the component style, create a theme extension in the project. A convenient way to do this is to use CUBA Studio: in the main menu, click CUBA > Advanced > Manage themes > Create theme extension. Select the hover theme in the popup window. Another way is to use the extend-theme command in CUBA CLI. After that, open the themes/hover/com.company.demo/hover-ext.scss file located in the web module and add the following code:

    /* Define your theme modifications inside next mixin */
    @mixin com_company_demo-hover-ext {
      /* 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
3.5.17.3.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 Data Components.

In order to integrate Stepper into CUBA Generic UI, we need to create the following files:

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

  • WebStepper - a component implementation in the gui subfolder of the web module.

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

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

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

Open the project in the IDE.

Let’s create required files and make necessary changes.

  • Create the Stepper interface in the gui subfolder of the web module. Replace its content with the following code:

    package com.company.demo.web.gui.components;
    
    import com.haulmont.cuba.gui.components.Field;
    
    // note that Stepper should extend Field
    public interface Stepper extends Field<Integer> {
    
        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.

  • Create the WebStepper class - a component implementation in the gui subfolder of the web module. Replace its content with the following code:

    package com.company.demo.web.gui.components;
    
    import com.haulmont.cuba.web.gui.components.WebV8AbstractField;
    import org.vaadin.risto.stepper.IntStepper;
    
    // note that WebStepper should extend WebV8AbstractField
    public class WebStepper extends WebV8AbstractField<IntStepper, Integer, Integer> implements Stepper {
    
        public WebStepper() {
            this.component = createComponent();
    
            attachValueChangeListener(component);
        }
    
        private IntStepper createComponent() {
            return new 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 WebV8AbstractField, which implements the methods of the Field interface.

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

    package com.company.demo.web.gui.xml.layout.loaders;
    
    import com.company.demo.web.gui.components.Stepper;
    import com.haulmont.cuba.gui.xml.layout.loaders.AbstractFieldLoader;
    
    public class StepperLoader extends AbstractFieldLoader<Stepper> {
        @Override
        public void createComponent() {
            resultComponent = factory.create(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. Replace its content with the following code:

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

    <?xml version="1.0" encoding="UTF-8" standalone="no"?>
    <xs:schema xmlns="http://schemas.company.com/agd/0.1/ui-component.xsd"
               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="height" type="xs:string"/>
                <xs:attribute name="width" type="xs:string"/>
    
                <xs:attribute name="dataContainer" 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.

  • Either remove the changes made in the previous section or generate editor screen for the entity.

  • Add the stepper component to the editor screen. You can add it either declaratively or programmatically. We’ll examine both methods.

    1. Using the component declaratively in an XML descriptor.

      • 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 form.

      • 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/screen/window.xsd"
              xmlns:app="http://schemas.company.com/agd/0.1/ui-component.xsd"
              caption="msg://editorCaption"
              focusComponent="form"
              messagesPack="com.company.demo.web.customer">
          <data>
              <instance id="customerDc"
                        class="com.company.demo.entity.Customer"
                        view="_local">
                  <loader/>
              </instance>
          </data>
          <dialogMode height="600"
                      width="800"/>
          <layout expand="editActions" spacing="true">
              <form id="form" dataContainer="customerDc">
                  <column width="250px">
                      <textField id="nameField" property="name"/>
                      <app:stepper id="stepper"
                                   dataContainer="customerDc" property="score"
                                   minValue="0" maxValue="20"/>
                  </column>
              </form>
              <hbox id="editActions" spacing="true">
                  <button action="windowCommitAndClose"/>
                  <button action="windowClose"/>
              </hbox>
          </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 customerDc instance container.

    2. Programmatic creation of the component in a Java controller.

      <?xml version="1.0" encoding="UTF-8" standalone="no"?>
      <window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd"
              caption="msg://editorCaption"
              focusComponent="form"
              messagesPack="com.company.demo.web.customer">
          <data>
              <instance id="customerDc"
                        class="com.company.demo.entity.Customer"
                        view="_local">
                  <loader/>
              </instance>
          </data>
          <dialogMode height="600"
                      width="800"/>
          <layout expand="editActions" spacing="true">
              <form id="form" dataContainer="customerDc">
                  <column width="250px">
                      <textField id="nameField" property="name"/>
                  </column>
              </form>
              <hbox id="editActions" spacing="true">
                  <button action="windowCommitAndClose"/>
                  <button action="windowClose"/>
              </hbox>
          </layout>
      </window>
      package com.company.demo.web.customer;
      
      import com.company.demo.entity.Customer;
      import com.company.demo.web.gui.components.Stepper;
      import com.haulmont.cuba.gui.UiComponents;
      import com.haulmont.cuba.gui.components.Form;
      import com.haulmont.cuba.gui.components.data.value.ContainerValueSource;
      import com.haulmont.cuba.gui.model.InstanceContainer;
      import com.haulmont.cuba.gui.screen.*;
      
      import javax.inject.Inject;
      
      @UiController("demo_Customer.edit")
      @UiDescriptor("customer-edit.xml")
      @EditedEntityContainer("customerDc")
      @LoadDataBeforeShow
      public class CustomerEdit extends StandardEditor<Customer> {
          @Inject
          private Form form;
          @Inject
          private InstanceContainer<Customer> customerDc;
          @Inject
          private UiComponents uiComponents;
      
          @Subscribe
          protected void onInit(InitEvent event) {
              Stepper stepper = uiComponents.create(Stepper.NAME);
              stepper.setValueSource(new ContainerValueSource<>(customerDc, "score"));
              stepper.setCaption("Score");
              stepper.setWidthFull();
              stepper.setMinValue(0);
              stepper.setMaxValue(20);
      
              form.add(stepper);
          }
      
          @Subscribe
          protected void onInitEntity(InitEntityEvent<Customer> event) {
              event.getEntity().setScore(0);
          }
      }
  • Start the application server. The resulting editor screen will look as follows:

customer edit result
3.5.17.3.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.

In order to use the Slider component, we need to create 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.

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.

Let’s create required files in the toolkit/ui/slider subfolder of the web module and make necessary changes.

  • 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.haulmont.cuba.web.widgets.WebJarResource;
    import com.vaadin.annotations.JavaScript;
    import com.vaadin.ui.AbstractJavaScriptComponent;
    import elemental.json.JsonArray;
    
    @WebJarResource({"jquery:jquery.min.js", "jquery-ui:jquery-ui.min.js", "jquery-ui:jquery-ui.css"})
    @JavaScript({"slider-connector.js"})
    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);
    
                getState(false).values = values;
    
                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();
        }
    
        @Override
        public SliderState getState(boolean markAsDirty) {
            return (SliderState) super.getState(markAsDirty);
        }
    
        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 @WebJarResource annotations point to files that must be loaded on the web page. In our example, these are JavaScript files of the jquery-ui library and the stylesheet for jquery-ui located in the WebJar resource, and the connector that is located in the Java package of the Vaadin server component.

  • 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 0px");
    
        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 Module field is set to Module: 'app-web_main' (this field is shown only if the gui module is added to the project).

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 a box that will be used as a container for a Vaadin component.

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/screen/window.xsd"
        caption="msg://editorCaption"
        focusComponent="form"
        messagesPack="com.company.jscomponent.web.product">
    <data>
        <instance id="productDc"
                  class="com.company.jscomponent.entity.Product"
                  view="_local">
            <loader/>
        </instance>
    </data>
    <dialogMode height="600"
                width="800"/>
    <layout expand="editActions" spacing="true">
        <form id="form" dataContainer="productDc">
            <column width="250px">
                <textField id="nameField" property="name"/>
                <textField id="minDiscountField" property="minDiscount" editable="false"/>
                <textField id="maxDiscountField" property="maxDiscount" editable="false"/>
                <hbox id="sliderBox" width="100%"/>
            </column>
        </form>
        <hbox id="editActions" spacing="true">
            <button action="windowCommitAndClose"/>
            <button action="windowClose"/>
        </hbox>
    </layout>
</window>

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

package com.company.jscomponent.web.product;

import com.company.jscomponent.entity.Product;
import com.company.jscomponent.web.toolkit.ui.slider.SliderServerComponent;
import com.haulmont.cuba.gui.components.HBoxLayout;
import com.haulmont.cuba.gui.screen.*;
import com.vaadin.ui.Layout;

import javax.inject.Inject;

@UiController("jscomponent_Product.edit")
@UiDescriptor("product-edit.xml")
@EditedEntityContainer("productDc")
@LoadDataBeforeShow
public class ProductEdit extends StandardEditor<Product> {

    @Inject
    private HBoxLayout sliderBox;

    @Subscribe
    protected void onInitEntity(InitEntityEvent<Product> event) {
        event.getEntity().setMinDiscount(15.0);
        event.getEntity().setMaxDiscount(70.0);
    }

    @Subscribe
    protected void onBeforeShow(BeforeShowEvent event) {
        SliderServerComponent slider = new SliderServerComponent();
        slider.setValue(new double[]{
                getEditedEntity().getMinDiscount(),
                getEditedEntity().getMaxDiscount()
        });
        slider.setMinValue(0);
        slider.setMaxValue(100);
        slider.setWidth("250px");
        slider.setListener(newValue -> {
            getEditedEntity().setMinDiscount(newValue[0]);
            getEditedEntity().setMaxDiscount(newValue[1]);
        });

        sliderBox.unwrap(Layout.class).addComponent(slider);
    }
}

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

Method onBeforeShow() initializes the slider component. 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
3.5.17.3.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.

Create the web-toolkit module. A convenient way to do this is to use CUBA Studio: in the main menu, click CUBA > Advanced > Manage modules > Create 'web-toolkit' Module.

In order to create a GTW component, we need to create the following files:

  • RatingFieldWidget.java - a GWT widget in the 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 create required files and make necessary changes in them.

  • Create the RatingFieldWidget class in the web-toolkit module. 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<>(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));
        }
    
        @Override
        protected void doSetValue(Integer value) {
            if (value == null) {
                value = 0;
            }
            getState().value = value;
        }
    
        @Override
        public Integer getValue() {
            return getState().value;
        }
    
        // define own state class
        @Override
        protected RatingFieldState getState() {
            return (RatingFieldState) super.getState();
        }
    
        @Override
        protected RatingFieldState getState(boolean markAsDirty) {
            return (RatingFieldState) super.getState(markAsDirty);
        }
    }
  • 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);
    }
  • Create the RatingFieldConnector class in the web-toolkit module. 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.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 = 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. A convenient way to do this is to use CUBA Studio: in the main menu, click CUBA > Advanced > Manage themes > Create theme extension. Select the hover theme in the popup window. Another way is to use the extend-theme command in CUBA CLI. The hover theme uses FontAwesome 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/hover/com.company.ratingsample 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 hover-ext.scss main theme file:

@import "components/ratingfield/ratingfield";

@mixin com_company_ratingsample-hover-ext {
  @include ratingfield;
}

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

Name the screen rating-screen.

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/screen/window.xsd"
        caption="msg://caption"
        messagesPack="com.company.ratingsample.web.screens.rating">
    <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.rating;

import com.company.ratingsample.web.toolkit.ui.RatingFieldServerComponent;
import com.haulmont.cuba.gui.components.VBoxLayout;
import com.haulmont.cuba.gui.screen.Screen;
import com.haulmont.cuba.gui.screen.Subscribe;
import com.haulmont.cuba.gui.screen.UiController;
import com.haulmont.cuba.gui.screen.UiDescriptor;
import com.vaadin.ui.Layout;

import javax.inject.Inject;

@UiController("ratingsample_RatingScreen")
@UiDescriptor("rating-screen.xml")
public class RatingScreen extends Screen {
    @Inject
    private VBoxLayout container;

    @Subscribe
    protected void onInit(InitEvent event) {
        RatingFieldServerComponent field = new RatingFieldServerComponent();
        field.setCaption("Rate this!");
        container.unwrap(Layout.class).addComponent(field);
    }
}

Start the application server and see the result.

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

Coming soon.

3.5.18. Generic UI Infrastructure

This section describes Generic UI infrastructure classes that can be extended in an application.

WebClientInfrastructure
Figure 27. Generic UI Infrastructure Classes
  • AppUI is a class inherited from com.vaadin.ui.UI. There is one instance of this class for each open web browser tab. It refers a RootWindow which contains either a login screen or main screen, depending on the connection state. You can get a reference to the AppUI for the current browser tab by using the AppUI.getCurrent() static method.

    If you want to customize functionality of AppUI in your project, create a class extending AppUI in the web module and register it in web-spring.xml with cuba_AppUI id and prototype scope, for example:

    <bean id="cuba_AppUI" class="com.company.sample.web.MyAppUI" scope="prototype"/>
  • Connection is an interface providing functionality of connecting to middleware and holding a user session. ConnectionImpl is a standard implementation of this interface.

    If you want to customize functionality of Connection in your project, create a class extending ConnectionImpl in the web module and register it in web-spring.xml with cuba_Connection id and vaadin scope, for example:

    <bean id="cuba_Connection" class="com.company.sample.web.MyConnection" scope="vaadin"/>
  • ExceptionHandlers class contains a collection of client-level exception handlers.

  • App contains links to Connection, ExceptionHandlers and other infrastructure objects. A single instance of this class is created for an HTTP session and stored in its attribute. You can get a reference to the App instance by using the App.getInstance() static method.

    If you want to customize functionality of App in your project, create a class extending DefaultApp in the web module and register it in web-spring.xml with cuba_App id and vaadin scope, for example:

    <bean name="cuba_App" class="com.company.sample.web.MyApp" scope="vaadin"/>

3.5.19. Web Login

This section describes how the web client authentication works and how to extend it in your project. For information about authentication on the middle tier, see Login.

Implementation of the login procedure of the Web Client block has the following mechanisms:

  • Connection implemented by ConnectionImpl.

  • LoginProvider implementations.

  • HttpRequestFilter implementations.

WebLoginStructure
Figure 28. Login mechanisms of the Web Client

The main interface of Web login subsystem is Connection which contains the following key methods:

  • login() - authenticates a user, starts a session and changes the state of the connection.

  • logout() - log out of the system.

  • substituteUser() - substitute a user in the current session with another user. This method creates a new UserSession instance, but with the same session ID.

  • getSession() - get the current user session.

After successful login, Connection sets UserSession object to the attribute of VaadinSession and sets SecurityContext. The Connection object is bound to VaadinSession thus it cannot be used from non-UI threads, it throws IllegalConcurrentAccessException in case of login/logout call from a non UI thread.

Usually, login is performed from the LoginScreen screen that supports login with login/password and "remember me" credentials.

The default implementation of Connection is ConnectionImpl, which delegates login to a chain of LoginProvider instances. A LoginProvider is a login module that can process a specific Credentials implementation, also it has a special supports() method to allow the caller to query if it supports a given Credentials type.

WebLoginProcedure
Figure 29. Standard user login process

Standard user login process:

  • Users enter their username and password.

  • Web client block creates a LoginPasswordCredentials object passing the login and password to its constructor and invokes Connection.login() method with this credentials.

  • Connection uses chain of LoginProvider objects. There is LoginPasswordLoginProvider that works with LoginPasswordCredentials instances. Depending on the cuba.checkPasswordOnClient it either invokes AuthenticationService.login(Credentials) passing user’s login and password; or loads the User entity by login, checks the password against the loaded password hash and logs in as a trusted client with TrustedClientCredentials and cuba.trustedClientPassword.

  • If the authentication is successful, the created AuthenticationDetails instance with the active UserSession is passed back to Connection.

  • Connection creates a ClientUserSession wrapper and sets it to VaadinSession.

  • Connection creates a SecurityContext instance and sets it to AppContext.

  • Connection fires StateChangeEvent that triggers UI update and leads to the MainScreen initialization.

All LoginProvider implementations must:

  • Authenticate user using Credentials object.

  • Start a new user session with AuthenticationService or return another active session (for instance, anonymous).

  • Return authentication details or null if it cannot login user with this Credentials object, for instance, if the login provider is disabled or is not properly configured.

  • Throw LoginException in case of incorrect Credentials or pass LoginException from the middleware to the caller.

HttpRequestFilter - marker interface for beans that will be automatically added to the application filter chain as HTTP filter: https://docs.oracle.com/javaee/6/api/javax/servlet/Filter.html. You can use it to implement additional authentication, pre- and post-processing of request and response.

You can expose additional Filter if you create Spring Framework component and implement HttpRequestFilter interface:

@Component
public class CustomHttpFilter implements HttpRequestFilter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain)
            throws IOException, ServletException {
        // delegate to the next filter/servlet
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {
    }
}

Please note that the minimal implementation has to delegate execution to FilterChain otherwise your application will not work. By default, filters added as HttpRequestFilter beans will not receive requests to VAADIN directory and other paths specified in cuba.web.cubaHttpFilterBypassUrls app property.

Built-in login providers

The platform contains the following implementations of LoginProvider interface:

  • AnonymousLoginProvider - provides anonymous login for non-logged-in users.

  • LoginPasswordLoginProvider - delegates login to AuthenticationService with LoginPasswordCredentials.

  • RememberMeLoginProvider- delegates login to AuthenticationService with RememberMeCredentials.

  • LdapLoginProvider - accepts LoginPasswordCredentials, performs authentication using LDAP and delegates login to AuthenticationService with TrustedClientCredentials.

  • ExternalUserLoginProvider - accepts ExternalUserCredentials and delegates login to AuthenticationService with TrustedClientCredentials. It can be used to perform login as a provided user name.

All the implementations create an active user session using AuthenticationService.login().

You can override any of them using Spring Framework mechanisms.

Events

Standard implementation of Connection - ConnectionImpl fires the following application events during login procedure:

  • BeforeLoginEvent / AfterLoginEvent

  • LoginFailureEvent

  • UserConnectedEvent / UserDisconnectedEvent

  • UserSessionStartedEvent / UserSessionFinishedEvent

  • UserSessionSubstitutedEvent

Event handlers of BeforeLoginEvent and LoginFailureEvent may throw LoginException to cancel login process or override the original login failure exception.

For instance, you can permit login to Web Client only for users with login that includes a company domain using BeforeLoginEvent.

@Component
public class BeforeLoginEventListener {
    @Order(10)
    @EventListener
    protected void onBeforeLogin(BeforeLoginEvent event) throws LoginException {
        if (event.getCredentials() instanceof LoginPasswordCredentials) {
            LoginPasswordCredentials loginPassword = (LoginPasswordCredentials) event.getCredentials();

            if (loginPassword.getLogin() != null
                    && !loginPassword.getLogin().contains("@company")) {
                throw new LoginException(
                        "Only users from @company are allowed to login");
            }
        }
    }
}

Additionally, the standard application class - DefaultApp fires the following events:

  • AppInitializedEvent - fired after App initialization, performed once per HTTP session.

  • AppStartedEvent - fired on the first request processing of an App right before login as anonymous user. Event handlers may login the user using the Connection object bound to App.

  • AppLoggedInEvent - fired after UI initialization of App when a user is logged in.

  • AppLoggedOutEvent - fired after UI initialization of App when a user is logged out.

  • SessionHeartbeatEvent - fired on heartbeat requests from a client web browser.

AppStartedEvent can be used to implement SSO login with third-party authentication system, for instance Jasig CAS. Usually, it is used together with a custom HttpRequestFilter bean that should collect and provide additional authentication data.

Let’s assume that we will automatically log in users if they have a special cookie value - PROMO_USER.

@Order(10)
@Component
public class AppStartedEventListener implements ApplicationListener<AppStartedEvent> {

    private static final String PROMO_USER_COOKIE = "PROMO_USER";

    @Inject
    private Logger log;

    @Override
    public void onApplicationEvent(AppStartedEvent event) {
        String promoUserLogin = event.getApp().getCookieValue(PROMO_USER_COOKIE);
        if (promoUserLogin != null) {
            Connection connection = event.getApp().getConnection();
            if (!connection.isAuthenticated()) {
                try {
                    connection.login(new ExternalUserCredentials(promoUserLogin));
                } catch (LoginException e) {
                    log.warn("Unable to login promo user {}: {}", promoUserLogin, e.getMessage());
                } finally {
                    event.getApp().removeCookie(PROMO_USER_COOKIE);
                }
            }
        }
    }
}

Thus if users have "PROMO_USER" cookie and open the application, they will be automatically logged in as promoUserLogin.

If you want to perform additional actions after login and UI initialization you could use AppLoggedInEvent. Keep in mind that you have to check if a user is authenticated or not in event handlers, all the events are fired for anonymous user as well.

Web Session Lifecycle Events

Depending on web session state two events can be published:

  • WebSessionInitializedEvent - fired when HTTP session is initialized.

  • WebSessionDestroyedEvent - fired when HTTP session is destroyed.

These events can be used to perform some system-level actions. Note that there is no SecurityContext available in the thread.

Extension points

You can extend login mechanisms using the following types of extension points:

  • Connection - replace existing ConnectionImpl.

  • HttpRequestFilter - implement additional HttpRequestFilter.

  • LoginProvider implementations - implement additional or replace existing LoginProvider.

  • Events - implement event handler for one of the available events.

You can replace existing beans using Spring Framework mechanisms, for instance by registering a new bean in Spring XML config of the web module.

<bean id="cuba_LoginPasswordLoginProvider"
      class="com.company.demo.web.CustomLoginProvider"/>

3.5.20. Anonymous Access to Screens

By default, only the login screen is available to the anonymous (not authenticated) session. By extending the login screen, you can add any information on it, or even add the WorkArea component and be able to open inside of it other screens available to the anonymous user. But as soon as the user logs in, all screens opened in the anonymous mode will be closed.

You may want to have some application screens to be always accessible regardless of whether the user is authenticated or not. Consider the following requirements:

  • When users open the application, they see a Welcome screen.

  • There is an Info screen with some publicly available information. The Info screen must be shown in a top level window, i.e. without main menu and other standard main window controls.

  • Users can open the Info screen both from the Welcome screen and by entering an URL in web browser.

  • Also from the Welcome screen, the user can go to login screen and continue working with the rest of the system as authenticated user.

Implementation steps are described below.

  1. Create the Info screen and annotate its controller with @Route to be able to open it using a link:

    <?xml version="1.0" encoding="UTF-8" standalone="no"?>
    <window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd"
            caption="msg://caption"
            messagesPack="com.company.demo.web.info">
        <layout margin="true">
            <label value="Info" stylename="h1"/>
        </layout>
    </window>
    package com.company.demo.web.info;
    
    import com.haulmont.cuba.gui.Route;
    import com.haulmont.cuba.gui.screen.*;
    
    @UiController("demo_InfoScreen")
    @UiDescriptor("info-screen.xml")
    @Route(path = "info") (1)
    public class InfoScreen extends Screen {
    }
    1 - specifies the screen’s address. When the screen is opened in the top-level window (as root), its address will be like http://localhost:8080/app/#info.
  2. Extend the default main screen in the project to implement the required Welcome screen. Use one of the Main screen …​ templates in the screen creation wizard in Studio, then add some components to the initialLayout element, for example:

    <?xml version="1.0" encoding="UTF-8" standalone="no"?>
    <window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd"
            xmlns:ext="http://schemas.haulmont.com/cuba/window-ext.xsd"
            extends="/com/haulmont/cuba/web/app/main/main-screen.xml">
        <layout>
            <hbox id="horizontalWrap">
                <workArea id="workArea">
                    <initialLayout>
                        <label id="welcomeLab" stylename="h1" value="Welcome!"/>
                        <button id="openInfoBtn" caption="Go to Info screen"/>
                    </initialLayout>
                </workArea>
            </hbox>
        </layout>
    </window>
    package com.company.demo.web.main;
    
    import com.company.demo.web.info.InfoScreen;
    import com.haulmont.cuba.gui.Screens;
    import com.haulmont.cuba.gui.components.Button;
    import com.haulmont.cuba.gui.screen.*;
    import com.haulmont.cuba.web.app.main.MainScreen;
    
    import javax.inject.Inject;
    
    @UiController("main")
    @UiDescriptor("ext-main-screen.xml")
    public class ExtMainScreen extends MainScreen {
    
        @Inject
        private Screens screens;
    
        @Subscribe("openInfoBtn")
        private void onOpenInfoBtnClick(Button.ClickEvent event) {
            screens.create(InfoScreen.class, OpenMode.ROOT).show(); (1)
        }
    }
    1 - create InfoScreen and open it in the root window when the user clicks the button.
  3. In order to open our Welcome screen instead of login screen when the user enters the application, add the following properties to the web-app.properties file:

    cuba.web.initialScreenId = main
    cuba.web.allowAnonymousAccess = true
  4. Enable Info screen for anonymous users: start the application, go to Administration > Roles and enable access to the Info screen for the Anonymous role.

As a result, when users open the application, they see the Welcome screen:

welcome_screen

Users can open Info screen without authentication or click the login button to enter the secured part of the application.

3.5.21. Unsupported Browser Page

If the browser’s version isn’t supported by the application, the user will see a standard page with a notification, a suggestion to update the browser and a list of recommended browsers.

Users will not be able to work with the application until they update the browser.

unsupported browser page
Figure 30. Unsupported browser page

You can change or localize the content of a default page. In order to do this, use the following keys in the main message pack of the web module:

  • unsupportedPage.captionMessage – notification caption;

  • unsupportedPage.descriptionMessage – notification description;

  • unsupportedPage.browserListCaption – caption of the browser’s list;

  • unsupportedPage.chromeMessage – message for Chrome browser;

  • unsupportedPage.firefoxMessage – message for Firefox browser;

  • unsupportedPage.safariMessage – message for Safari browser;

  • unsupportedPage.operaMessage – message for Opera browser;

  • unsupportedPage.edgeMessage – message for Edge browser;

  • unsupportedPage.explorerMessage – message for Explorer browser.

You can use a custom template for the unsupported browser page:

  1. Create a new *.html file template.

  2. Set the path to the new template in the cuba.web.unsupportedPagePath property of the web-app.properties file:

    cuba.web.unsupportedPagePath = /com/company/sample/web/sys/unsupported-page-template.html

3.6. GUI Legacy API

3.6.1. Screens (Legacy)

This is a legacy API. For new data API available since release 7.0, see Screens and Fragments.

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 default screen that should be opened after login can be set using the cuba.web.defaultScreenId application property.

The main menu of an application is generated on the basis of menu.xml files.

3.6.1.1. Screen Types

This is a legacy API. For new data API available since release 7.0, see Screen Controllers.

This section describes the following basic types of screens:

3.6.1.1.1. Frame

This is a legacy API. For new data API available since release 7.0, see Screen Controllers.

Frames are reusable parts of screens. Frames are included in screens using the frame XML element.

A frame controller must extend the AbstractFrame class.

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.

3.6.1.1.2. Simple Screen

This is a legacy API. For new data API available since release 7.0, see Screen Controllers.

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.

You can create a simple screen in Studio using the Blank screen template.

3.6.1.1.3. Lookup Screen

This is a legacy API. For new data API available since release 7.0, see Screen Controllers.

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.

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.

Customization of the lookup screen look and behavior
  • To change the lookup panel (Select and Cancel buttons) for all lookup screens in the project, create a frame and register it with the lookupWindowActions identifier. The default frame is in /com/haulmont/cuba/gui/lookup-window.actions.xml. Your frame must contain a button linked to the lookupSelectAction action (which is added automatically to the screen when it is opened as a lookup).

  • To replace the lookup panel in a certain screen, just create a button linked to the lookupSelectAction action in the screen. Then the default frame will not be added. For example:

    <layout expand="table">
        <hbox>
            <button id="selectBtn" caption="Select item"
                    action="lookupSelectAction"/>
        </hbox>
        <!-- ... -->
    </layout>
  • To replace the default select action with a custom one, just add your action in the controller:

    @Override
    public void init(Map<String, Object> params) {
        addAction(new SelectAction(this) {
            @Override
            protected Collection getSelectedItems(LookupComponent lookupComponent) {
                Set<MyEntity> selected = new HashSet<>();
                // ...
                return selected;
            }
        });
    }

    Use com.haulmont.cuba.gui.components.SelectAction as a base class for your action and override its methods when needed.

3.6.1.1.4. Edit Screen

This is a legacy API. For new data API available since release 7.0, see Screen Controllers.

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.

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.

3.6.1.1.5. Combined Screen

This is a legacy API. For new data API available since release 7.0, see Screen Controllers.

The combined screen allows you to display a list of entities on the left and an edit form for a selected entity on the right. So it is a combination of lookup and edit screens.

The controller of a combined screen should be inherited from the EntityCombinedScreen class.

You can create a combined screen for an entity in Studio using the Entity combined screen template.

3.6.1.2. XML-Descriptor

This is a legacy API. For the new API available since v.7.0, see Screen XML Descriptors.

XML-descriptor is a file in XML format describing datasources and screen layout.

Descriptor has the following structure:

window − root element.

window attributes:

  • class − name of a controller class.

  • messagesPack − a default message pack for the screen. It is used to obtain localized messages in the controller using getMessage() method and in the XML descriptor using message key without specifying the pack.

  • caption − window caption, can contain a link to a message from the above mentioned pack, for example,

    caption="msg://credits"
  • focusComponent − identifier of a component which should get input focus when the screen is displayed.

  • lookupComponent – mandatory attribute for a lookup screen; defines the identifier of a visual component that the entity instance should be selected from. Supports the following types of components (and their subclasses):

    • Table

    • Tree

    • LookupField

    • PickerField

    • OptionsGroup

  • datasource – mandatory attribute for an edit screen which defines the identifier of the datasource containing the edited entity instance.

window elements:

  • metadataContext − the element initializing the views required for the screen. It is recommended to define all views in a single views.xml file, because all view descriptors are deployed into a common repository, so it is difficult to ensure unique names if the descriptors are scattered across multiple files.

  • dsContext − defines datasource for the screen.

  • dialogMode - defines the settings of geometry and behaviour of the screen when it is opened as a dialog.

    Attributes of dialogMode:

    • closeable - defines whether the dialog window has close button. Possible values: true, false.

    • closeOnClickOutside - defines if the dialog window should be closed by click on outside the window area, when the window has a modal mode. Possible values: true, false.

    • forceDialog - specifies that the screen should always be opened as a dialog regardless of what WindowManager.OpenType was selected in the calling code. Possible values: true, false.

    • height - sets the height of the dialog window.

    • maximized - if the true value is set, the dialog window will be maximized across the screen. Possible values: true, false.

    • modal - specifies the modal mode for the dialog window. Possible values: true, false.

    • positionX - sets the x position of the top-left corner of the dialog window.

    • positionY - sets the y position of the top-left corner of the dialog window.

    • resizable - defines whether the user can change the size of the dialog window. Possible values: true, false.

    • width - sets the width of the dialog window.

    For example:

    <dialogMode height="600"
                width="800"
                positionX="200"
                positionY="200"
                forceDialog="true"
                closeOnClickOutside="false"
                resizable="true"/>
  • actions – defines the list of actions for the screen.

  • timers – defines the list of timers for the screen.

  • companions – defines the list of companion classes for the screen controller.

    Elements of companions:

    • web – defines a companion implemented in the web module.

    • desktop – defines a companion implemented in the desktop module.

    Each of these elements contains class attribute defining the companion class.

  • layout − root element of the screen layout, a container with a vertical layout of components.

3.6.1.3. Screen Controller

This is a legacy API. For the new API available since v.7.0, see Screen Controllers.

Screen controller is a Java or Groovy class, linked to an XML-descriptor and containing screen initialization and event handling logic.

Controller should be inherited from one of the following base classes:

If a screen does not need additional logic, it can use the base class itself as a controller – AbstractWindow, AbstractLookup or AbstractEditor, by specifying it in the XML-descriptor (these classes are not actually abstract in a sense of impossibility of instantiating). For frames, controller class can be omitted.

Controller class should be registered in class attribute of the root element window in a screen’s XML descriptor.

Controllers
Figure 31. Controller Base Classes
3.6.1.3.1. AbstractFrame

This is a legacy API. For the new API available since v.7.0, see Screen Controllers.

AbstractFrame is the root of the controller classes hierarchy. Below is the description of its main methods:

  • init() is called by the framework after creating components tree described by an XML-descriptor, but before a screen is displayed.

    init() method accepts a map of parameters that can be used in controller. These parameters can be passed both from the controller of the calling screen (using openWindow(), openLookup() or openEditor() methods) or defined in the screen registration file screens.xml.

    init() method should be implemented if it is necessary to initialize screen components, for example:

    @Inject
    private Table someTable;
    
    @Override
    public void init(Map<String, Object> params) {
        someTable.addGeneratedColumn("someColumn", new Table.ColumnGenerator<Colour>() {
            @Override
            public Component generateCell(Colour entity) {
                ...
            }
        });
    }
  • getMessage(), formatMessage() – methods for retrieving localized messages from a pack, defined for a screen in the XML-descriptor. They work as shortcuts for calling the corresponding methods of the Messages interface.

  • openFrame() – loads a frame according to an identifier registered in screens.xml file. If the method receives a container component from the invoking code, the frame is shown within the container. The method returns frame controller. For example:

    @Inject
    private BoxLayout container;
    
    @Override
    public void init(Map<String, Object> params) {
        SomeFrame frame = openFrame(container, "someFrame");
        frame.setHeight("100%");
        frame.someInitMethod();
    }

    It is not required to pass the container immediately via openFrame() method, instead it is possible to load the frame first and then add it to the necessary container:

    @Inject
    private BoxLayout container;
    
    @Override
    public void init(Map<String, Object> params) {
        SomeFrame frame = openFrame(null, "someFrame");
        frame.setHeight("100%");
        frame.someInitMethod();
        container.add(frame);
    }
  • openWindow(), openLookup(), openEditor() – open a simple screen, a lookup screen, or an edit screen respectively. Methods return a controller of the created screen.

    For the dialog mode, the method openWindow() can be called with parameters, for example:

    @Override
    public void actionPerform(Component component) {
        openWindow("sec$User.browse", WindowManager.OpenType.DIALOG.width(800).height(300).closeable(true).resizable(true).modal(false));
    }

    These parameters will be considered if they don’t conflict with the higher-priority parameters of the window being opened. The latter can be set either in the getDialogOptions() method of screen controller or in XML descriptor of the screen:

    <dialogMode forceDialog="true" width="300" height="200" closeable="true" modal="true" closeOnClickOutside="true"/>

    CloseListener can be added in order to perform actions after the invoked screen closes, for example:

    CustomerEdit editor = openEditor("sales$Customer.edit", customer, WindowManager.OpenType.THIS_TAB);
    editor.addCloseListener((String actionId) -> {
        // do something
    });

    Use CloseWithCommitListener to be notified only when the invoked screen closes by an action with the Window.COMMIT_ACTION_ID name (i.e. OK button), for example:

    CustomerEdit editor = openEditor("sales$Customer.edit", customer, WindowManager.OpenType.THIS_TAB);
    editor.addCloseWithCommitListener(() -> {
        // do something
    });
  • showMessageDialog() – shows a dialog box with a message.

  • showOptionDialog() – shows a dialog box with a message and an option for user to invoke certain actions. Actions are defined by an array of Action type items displayed as buttons in the dialog.

    It is recommended to use DialogAction objects for display of standard buttons such as OK, Cancel and other, for example:

    showOptionDialog("PLease confirm", "Are you sure?",
            MessageType.CONFIRMATION,
            new Action[] {
                new DialogAction(DialogAction.Type.YES) {
                    @Override
                    public void actionPerform(Component component) {
                        // do something
                    }
                },
                new DialogAction(DialogAction.Type.NO)
            });
  • showNotification() – shows a pop up notification.

  • showWebPage() – opens specified web page in a browser.



3.6.1.3.2. AbstractWindow

This is a legacy API. For the new API available since v.7.0, see Screen Controllers.

AbstractWindow is a subclass of AbstractFrame and defines the following methods:

  • getDialogOptions() – returns a DialogOptions object to control geometry and behaviour of the screen when it is opened as a dialog (WindowManager.OpenType.DIALOG). These options can be set when the screen is initialized as well as can be changed at a runtime. See the examples below.

    Setting the width and height:

    @Override
    public void init(Map<String, Object> params) {
        getDialogOptions().setWidth("480px").setHeight("320px");
    }

    Setting the dialog position on the screen:

    getDialogOptions()
            .setPositionX(100)
            .setPositionY(100);

    Making the dialog closeable by click on outside area:

    getDialogOptions().setModal(true).setCloseOnClickOutside(true);

    Making the dialog non-modal and resizable:

    @Override
    public void init(Map<String, Object> params) {
        getDialogOptions().setModal(false).setResizable(true);
    }

    Defining whether the dialog should be maximized across the screen:

    getDialogOptions().setMaximized(true);

    Specifying that the screen should always be opened as a dialog regardless of what WindowManager.OpenType was selected in the calling code:

    @Override
    public void init(Map<String, Object> params) {
        getDialogOptions().setForceDialog(true);
    }
  • setContentSwitchMode() - defines how the managed main TabSheet should switch a tab with the given window: hide it or unload its content.

    Three options are available:

    • DEFAULT - the switch mode is determined by the managed main TabSheet mode, defined in the cuba.web.managedMainTabSheetMode application property.

    • HIDE - the tab content should be hidden not considering the TabSheet mode.

    • UNLOAD - tab content should be unloaded not considering the TabSheet mode.

  • saveSettings() - saves the screen settings of the current user to the database when the screen is closed.

    For example, the screen contains a checkBox showPanel that manages some panel’s visibility. In the following method we create an XML element for the checkBox, then add an attribute showPanel containing the checkBox value to this element, and finally we save the settings element to the XML descriptor for the current user in the database:

    @Inject
    private CheckBox showPanel;
    
    @Override
    public void saveSettings() {
        boolean showPanelValue = showPanel.getValue();
        Element xmlDescriptor = getSettings().get(showPanel.getId());
        xmlDescriptor.addAttribute("showPanel", String.valueOf(showPanelValue));
        super.saveSettings();
    }
  • applySettings() - restores settings saved in the database for the current user when the screen is opened.

    This method can be overridden to restore custom settings. For example, in the method below we get the XML element of the checkBox from the previous example, then we make sure the required attribute is not null, and if it isn’t, we set the restored value to the checkBox:

    @Override
    public void applySettings(Settings settings) {
        super.applySettings(settings);
        Element xmlDescriptor = settings.get(showPanel.getId());
        if (xmlDescriptor.attribute("showPanel") != null) {
            showPanel.setValue(Boolean.parseBoolean(xmlDescriptor.attributeValue("showPanel")));
        }
    }

    Another example of managing settings is the standard Server Log screen from the Administration menu of CUBA application that automatically saves and restores recently opened log files.

  • ready() - a template method that can be implemented in controller to intercept the moment of screen opening. It is invoked when the screen is fully initialized and opened.

  • validateAll() – validates a screen. The default implementation calls validate() for all screen components implementing the Component.Validatable interface, collects information about exceptions and displays corresponding message. Method returns false, if any exceptions were found; and true otherwise.

    This method should be overridden only if it is required to override screen validation procedure completely. It is sufficient to implement a special template method – postValidate(), if validation should be just supplemented.

  • postValidate() – a template method that can be implemented in controller for additional screen validation. The method stores validation errors information in ValidationErrors object which is passed to it. Afterwards this information is displayed together with the errors of standard validation. For example:

    private Pattern pattern = Pattern.compile("\\d");
    
    @Override
    protected void postValidate(ValidationErrors errors) {
        if (getItem().getAddress().getCity() != null) {
            if (pattern.matcher(getItem().getAddress().getCity()).find()) {
                errors.add("City name can't contain digits");
            }
        }
    }
  • showValidationErrors() - shows validation errors alert. It can be overridden to change the default alert behavior. The notification type can be defined by the cuba.gui.validationNotificationType application property.

    @Override
    public void showValidationErrors(ValidationErrors errors) {
        super.showValidationErrors(errors);
    }
  • close() – closes this screen.

    The method accepts string value, which is then passed to preClose() template method and to CloseListener listeners. Thus, the information about the reason why the window was closed can be obtained from the code that initiated the closing event. It is recommended to use the following constants for closing edit screens: Window.COMMIT_ACTION_ID after committing changes, Window.CLOSE_ACTION_ID – without committing changes.

    If any of the datasources contains unsaved changes, a dialog with a corresponding message will be displayed before the screen is closed. Notification type may be adjusted using the cuba.gui.useSaveConfirmation application property.

    A variant of close() method with force = true parameter closes the screen without calling preClose() and without a notification regardless of any unsaved changes.

    close() method returns true, if the screen is closed successfully, and false – if closing procedure was interrupted.

  • preClose() is a template method which can be implemented in a controller to intercept the moment when the window closes. The method receives a string value provided by the closing initiator when invoking close() method.

    If the preClose() method returns false, the window closing process is interrupted.

  • addBeforeCloseWithCloseButtonListener() - adds a listener to be notified when a screen is closed with one of the following approaches: the screen’s close button, bread crumbs, or TabSheet tabs' close actions (Close, Close All, Close Others). To prevent a user from closing a window accidentally, invoke the preventWindowClose() method of BeforeCloseEvent:

    addBeforeCloseWithCloseButtonListener(BeforeCloseEvent::preventWindowClose);
  • addBeforeCloseWithShortcutListener - adds a listener to be notified when a screen is closed with a close shortcut (for example, Esc button). To prevent a user from closing a window accidentally, invoke the preventWindowClose() method of BeforeCloseEvent:

    addBeforeCloseWithShortcutListener(BeforeCloseEvent::preventWindowClose);


3.6.1.3.3. AbstractLookup

This is a legacy API. For the new API available since v.7.0, see Screen Controllers.

AbstractLookup is the base class for lookup screen controllers. It is a subclass of AbstractWindow and defines the following own methods:

  • setLookupComponent() – sets the component, which will be used to select entity instances.

    As a rule, component for selection is defined in screen XML-descriptor and there is no need to call this method in the application code.

  • setLookupValidator() – sets Window.Lookup.Validator object to the screen, which validate() method is invoked by the framework before returning selected entity instances. If validate() method returns false, the lookup and window closing process is interrupted.

    By default, the validator is not set.



3.6.1.3.4. AbstractEditor

This is a legacy API. For the new API available since v.7.0, see Screen Controllers.

AbstractEditor is the base class for edit screen controller. It is a subclass of AbstractWindow.

When creating a controller class, it is recommended to parameterize AbstractEditor with the edited entity class. This enables getItem() and initNewItem() methods work with the specified entity type and application code does not need to do additional type conversion. For example:

public class CustomerEdit extends AbstractEditor<Customer> {

    @Override
    protected void initNewItem(Customer item) {
        ...

AbstractEditor defines the following own methods:

  • getItem() – returns an instance of the entity being edited, which is set in the main datasource of the screen (i.e. specified in the datasource attribute of the root element of the XML-descriptor).

    If the instance being edited is not a new one, screen opening procedure will reload the instance from the database with the required view as set for the main datasource.

    Changes made to the instance returned by getItem(), are reflected in the state of the datasource and will be sent to the Middleware at commit.

    It should be considered that getItem() returns a value only after screen is initialized with setItem() method. Until this moment, this method returns null, for instance when calling from inside init() or initNewItem().

    However, in the init() method, an instance of an entity passed to openEditor() can be retrieved from parameters using the following approach:

    @Override
    public void init(Map<String, Object> params) {
        Customer item = WindowParams.ITEM.getEntity(params);
        // do something
    }

    The initNewItem() method receives an instance as a parameter of the appropriate type.

    In both cases the obtained entity instance will be reloaded afterwards unless it is a new one. Therefore you should not change it or save it in a field for future use.

  • setItem() – invoked by the framework when a window is opened using openEditor() to set the instance being edited to the main datasource. By the moment of invocation all screen components and datasources will have been created and the controller’s init() method will have been executed.

    It is recommended to use template methods initNewItem() and postInit(), instead of overriding setItem() in order to initialize a screen.

  • initNewItem() – a template method invoked by the framework before setting the edited entity instance into the main datasource.

    The initNewItem() method is called for newly created entity instances only. The method is not called for detached instances. This method can be implemented in the controller, if new entity instances must be initialized before setting them in the datasource. For example:

    @Inject
    private UserSession userSession;
    
    @Override
    protected void initNewItem(Complaint item) {
        item.setOpenedBy(userSession.getUser());
        item.setStatus(ComplaintStatus.OPENED);
    }

    A more complex example of using the initNewItem() method can be found in the cookbook.

  • postInit() – a template method invoked by the framework immediately after the edited entity instance is set to the main datasource. In this method, getItem() can be called to return a new entity instance or an instance re-loaded during screen initialization.

    This method can be implemented in controller for final screen initialization, for example:

    @Inject
    private EntityStates entityStates;
    @Inject
    protected EntityDiffViewer diffFrame;
    
    @Override
    protected void postInit() {
        if (!entityStates.isNew(getItem())) {
            diffFrame.loadVersions(getItem());
        }
    }
  • commit() – validates the screen and submits changes to the Middleware via DataSupplier.

    If a method is used with validate = false, commit does not perform a validation.

    It is recommended to use specialized template methods – postValidate(), preCommit() and postCommit() instead of overriding this method.

  • commitAndClose() – validates the screen, submits changes to the Middleware and closes the screen. The value of the Window.COMMIT_ACTION_ID will be passed to the preClose() method and registered CloseListener listeners.

    It is recommended to use specialized template methods – postValidate(), preCommit() and postCommit() instead of overriding this method.

  • preCommit() – a template method invoked by the framework during the commit process, after a successful validation, but before the data is submitted to the Middleware.

    This method can be implemented in controller. If the method returns false, commit process gets interrupted, as well as window closing process (if commitAndClose() was invoked). For example:

    @Override
    protected boolean preCommit() {
        if (somethingWentWrong) {
            notifications.create()
                    .withCaption("Something went wrong")
                    .withType(Notifications.NotificationType.WARNING)
                    .show();
            return false;
        }
        return true;
    }
  • postCommit() – a template method invoked by the framework at the final stage of committing changes. Method parameters are:

    • committed – set to true, if the screen had changes and they have been submitted to Middleware.

    • close – set to true, if the screen should be closed after the changes are committed.

      If the screen does not close the default implementation of this method displays a message about successful commit and invokes postInit().

      This method can be overridden in controller in order to perform additional actions after successful commit, for example:

      @Inject
      private Datasource<Driver> driverDs;
      @Inject
      private EntitySnapshotService entitySnapshotService;
      
      @Override
      protected boolean postCommit(boolean committed, boolean close) {
          if (committed) {
              entitySnapshotService.createSnapshot(driverDs.getItem(), driverDs.getView());
          }
          return super.postCommit(committed, close);
      }

The diagrams below show initialization sequence and different ways to commit changes for an edit screen.

EditorInit
Figure 32. Edit Screen Initialization
EditorCommit
Figure 33. Committing And Closing a Window With an editWindowActions Frame
ExtendedEditorCommit
Figure 34. Committing a Screen With an extendedEditWindowActions Frame
ExtendedEditorCommitAndClose
Figure 35. Committing a Screen With an extendedEditWindowActions Frame


3.6.1.3.5. EntityCombinedScreen

This is a legacy API. For the new API available since v.7.0, see Screen Controllers.

EntityCombinedScreen is the base class for combined screen controllers. It is a subclass of AbstractLookup.

The EntityCombinedScreen class looks up key components such as table, field group and others by hardcoded identifiers. If you name your components differently, override protected methods of the class and return your identifiers to let the controller find your components. See the class JavaDocs for details.

3.6.1.3.6. Controller Dependency Injection

This is a legacy API. For the new API available since v.7.0, see Screen Controllers.

Dependency Injection in controllers can be used to acquire references to utilized objects. For this purpose it is required to declare either a field of the corresponding type or a write access method (setter) with an appropriate parameter type and with one of the following annotations:

  • @Inject – the simplest option, where an object for injection will be found according to the field/method type and the name of the field or attribute corresponding to the method according to JavaBeans rules.

  • @Named("someName") – explicitly defines the name of the target object.

The following objects can be injected into controllers:

  • This screen’s visual components defined in the XML-descriptor. If the attribute type is derived from Component, the system will search for a component with the corresponding name within the current screen.

  • Actions defined in the XML-descriptor – see Actions. The Action Interface.

  • Datasources defined in the XML-descriptor. If the attribute type is derived from Datasource, the system will search for a datasource with the corresponding name in the current screen.

  • UserSession. If the attribute type is UserSession, the system will inject an object of the current user session.

  • DsContext. If the attribute type is DsContext, the system will inject the DsContext of the current screen.

  • WindowContext. If the attribute type is WindowContext, the system will inject the WindowContext of the current screen.

  • DataSupplier. If the attribute type is DataSupplier, the corresponding instance will be injected.

  • Any bean defined in the context of a given client block, including:

  • If nothing of the mentioned above is appropriate and the controller has companions, a companion for the current client type will be injected, if the types match.

It is possible to inject parameters passed in a map to the init() method into the controller using @WindowParam annotation. The annotation has the name attribute which contains the parameter name (a key in the map) and an optional required attribute. If required = true and the map does not contain the corresponding parameter a WARNING message is added to the log.

An example of the injection of a Job entity passed to the controller’s init() method:

@WindowParam(name = "job", required = true)
protected Job job;
3.6.1.3.7. Controller Companions

This section is not relevant since version 7.0 as the desktop client is not supported anymore. Instead of creating companions, just place your screen in the web module.

3.6.1.4. Screen Agent

Screen agent support has been removed without replacement in v.7.0. You can get DeviceInfo using the DeviceInfoProvider bean and either create different screens for each device type or open fragments in a screen.

3.6.2. Datasources (Legacy)

This is a legacy API. For new data API available since release 7.0, see Data Components.

Datasources provide data to data-aware components.

Visual components themselves do not access Middleware: they get entity instances from linked datasources. Furthermore, one datasource can work with multiple visual components if they need the same instance or set of instances.

  • When a user changes a value in the component, the new value is set for the entity attribute in the datasource.

  • When the entity attribute is modified in the code, the new value is set and displayed in the visual component.

  • User input can be monitored both by datasource listeners and value listeners on the component – they are notified sequentially.

  • To read or write the value of an attribute in the application code, it is recommended to use the datasource, rather than the component. Below is an example of reading the attribute:

    @Inject
    private FieldGroup fieldGroup;
    
    @Inject
    private Datasource<Order> orderDs;
    
    @Named("fieldGroup.customer")
    private PickerField customerField;
    
    public void init(Map<String, Object> params){
        Customer customer;
        // Get customer from component: not for common use
        Component component = fieldGroup.getFieldNN("customer").getComponentNN();
        customer = ((HasValue)component).getValue();
        // Get customer from component
        customer = customerField.getValue();
        // Get customer from datasource: recommended
        customer = orderDs.getItem().getCustomer();
    }

    As you can see, working with entity attribute values through a component is not very straightforward. In the first example, it requires type casting and specifying FieldGroup field id as a string. The second example is more safe and direct, but requires you to know exactly the type of the field to be injected. At the same time, if the instance is obtained from the datasource via the getItem() method, the values of attributes can be read and modified directly.

Datasources also track changes in entities contained therein and can send modified instances back to the middleware for storing in the database.

Typically, a visual component is bound to an attribute that directly belongs to the entity in the datasource. In the example above, the component is bound to the customer attribute of the Order entity.

A component can also be associated with an attribute of a related entity, for example, customer.name. In this case, the component will display the value of the name attribute, however when the user changes the value, the datasource listeners will not be invoked and the changes will not be saved. Therefore, it makes sense to bind the component to second-level entity attributes only if they are intended for display. For example in a Label, a Table column, or in a TextField, where editable = false.

The basic interfaces of datasources are described below.

Datasources
Figure 36. Datasource interfaces
  • Datasource is a simple datasource designed to work with one entity instance. The instance is set by the setItem() method and is accessed via getItem().

    DatasourceImpl class is the standard implementation of such datasource, which is used, for instance, as a main datasource on entity edit screens.

  • CollectionDatasource is a datasource designed to work with a collection of entity instances. The collection is loaded with the invocation of the refresh() method, instance keys are accessible through the getItemIds() method. The setItem() method sets the "current" instance of the collection and getItem() returns it (for example, the one that corresponds to the currently selected table row).

    The way of loading collections is determined by implementation. The most typical one is loading from Middleware via DataManager; in this case, setQuery(), setQueryFilter() are used to form a JPQL query.

    CollectionDatasourceImpl class is the standard implementation of such datasources, which is used on screens with entity lists.

    • GroupDatasource is a subtype of CollectionDatasource, designed to work with the GroupTable component.

      Standard implementation is the GroupDatasourceImpl class.

    • HierarchicalDatasource is a subtype of CollectionDatasource, designed to work with the Tree and TreeTable components.

      Standard implementation is the HierarchicalDatasourceImpl class.

  • NestedDatasource is a datasource designed to work with instances that are loaded in an attribute of another entity. In this case, a datasource that contains a parent entity is accessible via getMaster(), and meta property that corresponds to the parent attribute containing instances of this datasource is accessible via getProperty().

    For example an Order instance which contains a reference to the Customer instance is set in the dsOrder datasource. Then, to link the Customer instance with visual components, it is enough to create NestedDatasource with dsOrder as parent and meta property to point to the Order.customer attribute.

    • PropertyDatasource is a subtype of NestedDatasource, designed to work with one instance or collection of related entities that are not embedded.

      Standard implementations: for working with one instance – PropertyDatasourceImpl, with a collection – CollectionPropertyDatasourceImpl, GroupPropertyDatasourceImpl, HierarchicalPropertyDatasourceImpl. The latter also implements the CollectionDatasource interface, however some of its irrelevant methods like setQuery() throw UnsupportedOperationException.

    • EmbeddedDatasource is a subtype of NestedDatasource, which contains an instance of an embedded entity.

      Standard implementation is the EmbeddedDatasourceImpl class.

  • RuntimePropsDatasource is a specific datasource, designed to work with dynamic attributes of entities.

Typically, datasources are declared in the dsContext section of a screen descriptor.

Automatic CollectionDatasource refresh

When the screen opens, its visual components connected to collection datasources cause the datasources to load data. As a result, tables show data right after opening the screen, without any explicit user action. If you want to prevent automatic loading of collection datasources, set the DISABLE_AUTO_REFRESH screen parameter to true in the screen’s init() method or pass it from the calling code. This parameter is defined in the WindowParams enumeration, so it can be set as shown below:

@Override
public void init(Map<String, Object> params) {
    WindowParams.DISABLE_AUTO_REFRESH.set(params, true);
}

In this case, the screen collection datasources will be loaded only when their refresh() method will be called. It can be done by the application code or when the user clicks Search in the Filter component.

3.6.2.1. Creating Datasources

This is a legacy API. For new data API available since release 7.0, see Data Components.

Datasource objects can be created both declaratively, using an XML screen descriptor, and programmatically in a controller. Typically, standard implementation of datasources is used, however, you can create your own class that is inherited from a standard one, if necessary.

3.6.2.1.1. Declarative Creation

This is a legacy API. For new data API available since release 7.0, see Data Components.

Typically, datasources are declared in the dsContext element of a screen descriptor. Depending on the relative position of declaration elements, datasources of two varieties are created:

  • if an element is located directly in dsContext, a normal Datasource or CollectionDatasource, which contains an independently loaded entity or collection, is created;

  • if an element is located inside an element of another datasource, NestedDatasource is created and the external datasource becomes its parent.

Below is an example of declaring a datasource:

<dsContext>
    <datasource id="carDs" class="com.haulmont.sample.entity.Car" view="carEdit">
        <collectionDatasource id="allocationsDs" property="driverAllocations"/>
        <collectionDatasource id="repairsDs" property="repairs"/>
    </datasource>

    <collectionDatasource id="colorsDs" class="com.haulmont.sample.entity.Color" view="_local">
        <query>
            <![CDATA[select c from sample$Color c order by c.name]]>
        </query>
    </collectionDatasource>
</dsContext>

In the example above, carDs contains one entity instance, Car, and nested allocationsDs and repairsDs contain collections of related entities from the Car.driverAllocations and Car.repairs attributes, respectively. The Car instance together with related entities is set into the datasource from the outside. If this screen is an edit screen, it happens automatically when opening the screen. The colorsDs datasource contains a collection of instances of the Color entity, which is loaded by the datasource itself using the specified JPQL query with the _local view.

Below is the XML scheme.

dsContext – root element.

dsContext elements:

  • datasource – defines a datasource that contains a single entity instance.

    Attributes:

    • id – datasource identifier, must be unique for this DsContext.

    • class – Java class of an entity that will be contained in this datasource.

    • view – name of entity view. If the datasource itself loads instances, then this view will be used during loading. Otherwise, this view makes signals to external mechanisms on how to load an entity for this datasource.

    • allowCommit – if set to false, the isModified() method of this datasource always returns false and the commit() method does nothing. Thus, changes in entities that are contained in the datasource are ignored. By default, it is set to true, i.e., changes are tracked and can be saved.

    • datasourceClass is a custom implementation class, if necessary.

  • collectionDatasource – defines a datasource that contains a collection of instances.

    collectionDatasource attributes:

    • refreshMode – a datasource update mode, default is ALWAYS. In the NEVER mode, when refresh() method is invoked, the datasource does not load data and only changes its state to Datasource.State.VALID, notifies listeners and sorts available instances. The NEVER mode is useful if you need to programmatically fill CollectionDatasource with preloaded or created entities. For example:

      @Override
      public void init(Map<String, Object> params) {
          Set<Customer> entities = (Set<Customer>) params.get("customers");
          for (Customer entity : entities) {
              customersDs.includeItem(entity);
          }
          customersDs.refresh();
      }
    • softDeletion – the false value disables the soft deletion mode when loading entities, i.e., deleted instances will also be loaded. Default value is true.

    collectionDatasource elements:

    • query – query to load entities

  • groupDatasource – completely similar to collectionDatasource, but creates datasource implementation that is suitable to use in conjunction with the GroupTable component.

  • hierarchicalDatasource – similar to collectionDatasource, and creates datasource implementation that is suitable to use in conjunction with the Tree and TreeTable components.

    hierarchyProperty is a specific attribute. It specifies an attribute name, upon which a hierarchy is built.

A datasource implementation class is selected implicitly based on the name of the XML element and, as mentioned above, the mutual arrangement of elements. However, if you need to apply a custom datasource, you can explicitly specify its class in the datasourceClass attribute.

3.6.2.1.2. Programmatic Creation

This is a legacy API. For new data API available since release 7.0, see Data Components.

If you need to create a datasource in the Java code, it is recommended to use a special class, DsBuilder.

The DsBuilder instance is parameterized by an invocation chain of its methods in the fluent interface style. If the master and property parameters are set, then NestedDatasource will be created, otherwise – Datasource or CollectionDatasource.

Example:

CollectionDatasource ds = new DsBuilder(getDsContext())
        .setJavaClass(Order.class)
        .setViewName(View.LOCAL)
        .setId("ordersDs")
        .buildCollectionDatasource();
3.6.2.1.3. Custom Implementation Classes

This is a legacy API. For new data API available since release 7.0, see Data Components.

If you need to implement a custom mechanism of loading entities, create a custom datasource class inherited from CustomCollectionDatasource, CustomGroupDatasource, or CustomHierarchicalDatasource, and implement the getEntities() method.

For example:

public class MyDatasource extends CustomCollectionDatasource<SomeEntity, UUID> {

    private SomeService someService = AppBeans.get(SomeService.NAME);

    @Override
    protected Collection<SomeEntity> getEntities(Map<String, Object> params) {
        return someService.getEntities();
    }
}

To create a custom datasource instance declaratively, specify the custom class name in the datasourceClass attribute of the datasource XML element. In case of programmatic creation via DsBuilder, specify the class by invoking setDsClass() or as a parameter of one of the build*() methods.

3.6.2.2. CollectionDatasourceImpl Queries

This is a legacy API. For new data API available since release 7.0, see Data Components.

The CollectionDatasourceImpl class and its subclasses, GroupDatasourceImpl and HierarchicalDatasourceImpl, are standard implementations of datasources that work with collections of entity instances. These datasources load data via DataManager by sending a JPQL queries to the middleware. The format of these queries is described below.

3.6.2.2.1. Returned values

This is a legacy API. For new data API available since release 7.0, see Data Components.

A query should return entities of the type which is specified at the moment of creating a datasource. In case of declarative creation, the entity type is specified in the class attribute of an XML element, if DsBuilder is used – in the setJavaClass() or setMetaClass() method.

For example, a query of the datasource of the Customer entity may look as follows:

select c from sales$Customer c

or

select o.customer from sales$Order o

A query cannot return single attributes or aggregates, for example:

select c.id, c.name from sales$Customer c /* invalid – returns single fields, not the whole Customer object */

If you need to execute a query which returns scalar values or aggregates and to display the results in visual components using standard data binding, use Value Datasources.

3.6.2.2.2. Query Parameters

This is a legacy API. For new data API available since release 7.0, see Data Components.

A JPQL query in a datasource may contain parameters of several types. A parameter type is determined by a prefix of a parameter name. A prefix is a part of the name before the $ character. The interpretation of the name after $ is described below.

  • The ds prefix

    The parameter value is data from another datasource that is registered in the same DsContext. For example:

    <collectionDatasource id="customersDs" class="com.sample.sales.entity.Customer" view="_local">
        <query>
             <![CDATA[select c from sales$Customer c]]>
        </query>
    </collectionDatasource>
    
    <collectionDatasource id="ordersDs" class="com.sample.sales.entity.Order" view="_local">
        <query>
             <![CDATA[select o from sales$Order o where o.customer.id = :ds$customersDs]]>
        </query>
    </collectionDatasource>

    In the example above, a query parameter of the ordersDs datasource will be a current entity instance located in the customersDs datasource.

    If parameters with the ds prefix are used, dependencies between datasources are created automatically. They lead to updating the datasource if its parameter are changed. In the example above, if the selected Customer is changed, the list of its Orders is changed automatically.

    Please note that in the example of the parameterized query, the left part of the comparison operator is the value of the o.customer.id identifier, and the right part – the Customer instance that is contained in the customersDs datasource. This comparison is valid since when running a query at Middleware, the implementation of the Query interface, by assigning values to query parameters, automatically adds entity ID instead of a passed entity instance.

    A path through the entity graph to an attribute (from which the value should be used) can be specified in the parameter name after the prefix and name of a datasource, for example:

    <query>
        <![CDATA[select o from sales$Order o where o.customer.id = :ds$customersDs.id]]>
    </query>

    or

    <query>
        <![CDATA[select o from sales$Order o where o.tagName = :ds$customersDs.group.tagName]]>
    </query>
  • The custom prefix.

    A parameter value will be taken from the Map<String, Object> object that is passed into the refresh() method of a datasource. For example:

    <collectionDatasource id="ordersDs" class="com.sample.sales.entity.Order" view="_local">
        <query>
            <![CDATA[select o from sales$Order o where o.number = :custom$number]]>
        </query>
    </collectionDatasource>
    ordersDs.refresh(ParamsMap.of("number", "1"));

    Casting an instance to its identifier, if necessary, is performed similarly to parameters with the ds prefix. The path through the entity graph in the parameter name is not supported in this case.

  • The param prefix.

    A parameter value is taken from the Map<String, Object> object that is passed into the init() method of a controller. For example:

    <query>
        <![CDATA[select e from sales$Order e where e.customer = :param$customer]]>
    </query>
    openWindow("sales$Order.lookup", WindowManager.OpenType.DIALOG, ParamsMap.of("customer", customersTable.getSingleSelected()));

    Casting an instance to its identifier, if necessary, is performed similarly to parameters with the ds prefix. The path through the entity graph in the parameter name is supported.

  • The component prefix.

    A parameter value will be a current value of a visual component, which path is specified in the parameter name. For example:

    <query>
        <![CDATA[select o from sales$Order o where o.number = :component$filter.orderNumberField]]>
    </query>

    The path to a component should include all nested frames.

    Casting an instance to its identifier, if necessary, is similar to ds parameters. The path through the entity graph in the parameter name is supported as the continuation of the path to a component in this case.

    The datasource will not be refreshed automatically if the component value is changed.

  • The session prefix.

    A parameter value will be a value of the user session attribute specified in the parameter name.

    The value is extracted by the UserSession.getAttribute() method, so predefined names of session attributes are also supported.

    • userId – ID of the currently registered or substituted user;

    • userLogin – login of the currently registered or substituted user in lowercase.

    Example:

    <query>
        <![CDATA[select o from sales$Order o where o.createdBy = :session$userLogin]]>
    </query>

    Casting an instance to its identifier, if necessary, is similar to ds parameters. In this case, the path through the entity graph in the parameter name is not supported.

3.6.2.2.3. Query Filter

This is a legacy API. For new data API available since release 7.0, see Data Components.

A datasource query can be modified at runtime depending on conditions entered by the user. This allows you to efficiently filter data at the database level.

The easiest way to provide such ability is to connect the datasource to a special visual component: Filter.

If the universal filter is not suitable for some reason, a special XML markup can be embedded into the query text. This allows you to create a resulting query based on values entered by the user into any visual components of the screen.

The following elements can be used in this filter:

  • filter – a root element of the filter. It can directly contain only one condition.

    • and, or – logical conditions, may contain any number of other conditions and statements.

    • c – JPQL condition, which is added to the where section of the query. If the query does not contain where clause, it will be added before the first condition. An optional join attribute can be used to specify joined entities. The value of the join attribute is added after the root entity declaration as is, so it must contain join keyword or comma.

Conditions and statements are added to the resulting query only if corresponding parameters have values, i.e. when they are not null.

Use only custom, param, component and session parameters in query filters. The ds parameter will not work as expected.

Example:

<query>
    <![CDATA[select distinct d from app$GeneralDoc d]]>
    <filter>
        <or>
            <and>
                <c join=", app$DocRole dr">dr.doc.id = d.id and d.processState = :custom$state</c>
                <c>d.barCode like :component$barCodeFilterField</c>
            </and>
            <c join=", app$DocRole dr">dr.doc.id = d.id and dr.user.id = :custom$initiator</c>
        </or>
    </filter>
</query>

In this case, if state and initiator parameters are passed to the refresh() method of the datasource, and the barCodeFilterField visual component has some value, then the resulting query will be as follows:

select distinct d from app$GeneralDoc d, app$DocRole dr
where
(
  (dr.doc.id = d.id and d.processState = :custom$state)
  and
  (d.barCode like :component$barCodeFilterField)
)
or
(dr.doc.id = d.id and dr.user.id = :custom$initiator)

If, for example, the barCodeFilterField component is empty and only initiator parameter is passed to the refresh() method, the query will be as follows:

select distinct d from app$GeneralDoc d, app$DocRole dr
where
(dr.doc.id = d.id and dr.user.id = :custom$initiator)
3.6.2.2.4. Case-Insensitive Search for a Substring

This is a legacy API. For new data API available since release 7.0, see Data Components.

It is possible to use a special feature of JPQL queries execution in datasources, described for the Query interface of the Middleware level: for easy creation of case-insensitive search condition of any substring, (?i) prefix can be used. However, due to the fact that the query value is usually passed implicitly, the following differences take place:

  • The (?i) prefix should be specified before a parameter name and not inside the value.

  • The parameter value will be automatically converted to lowercase.

  • If the parameter value does not have % characters, they will be added to the beginning and the end.

Below is an example of how to process the following query:

select c from sales$Customer c where c.name like :(?i)component$customerNameField

In this case, the parameter value taken from the customerNameField component will be converted to lowercase and will be framed with % characters, and then an SQL query with a lower(C.NAME) like ? condition will be executed in the database.

Please note that with this search, an index created in the DB by the NAME field, will not be used.

3.6.2.3. Value Datasources

This is a legacy API. For new data API available since release 7.0, see Data Components.

Value datasources enable execution of queries that return scalar values and aggregates. For example, you can load some aggregated statistics for customers:

select o.customer, sum(o.amount) from demo$Order o group by o.customer

Value datasources work with entities of a special type named KeyValueEntity. This entity can contain an arbitrary number of attributes which are defined at runtime. So in the example above, the KeyValueEntity instances will contain two attributes: the first of type Customer and the second of type BigDecimal.

Value datasource implementations extend other widely used collection datasource classes and implement a specific interface: ValueDatasource. Below is a diagram showing the value datasource implementations and their base classes:

ValueDatasources

The ValueDatasource interface declares the following methods:

  • addProperty() - as the datasource can return entities with any number of attributes, you have to specify what attributes are expected by using this method. It accepts a name of the attribute and its type in the form of Datatype or a Java class. In the latter case, the class should be either an entity class or a class supported by one the datatypes.

  • setIdName() is an optional method which allows you to define one of the attributes as an identifier attribute of the entity. It means that KeyValueEntity instances contained in this datasource will have identifiers obtained from the given attribute. Otherwise, KeyValueEntity instances get randomly generated UUIDs.

  • getMetaClass() returns a dynamic implementation of the MetaClass interface that represents the current schema of KeyValueEntity instances. It is defined by previous calls to addProperty().

Value datasources can be used declaratively in a screen XML descriptor. There are three XML elements corresponding to the implementation classes:

  • valueCollectionDatasource

  • valueGroupDatasource

  • valueHierarchicalDatasource

XML definition of a value datasource must contain the properties element that defines the attributes of KeyValueEntity instances that will be contained in the datasource (see the addProperty() method description above). The order of property elements should conform to the order of result set columns returned by the query. For example, in the following definition the customer attribute will get its value from o.customer column and the sum attribute from sum(o.amount) column:

<dsContext>
    <valueCollectionDatasource id="salesDs">
        <query>
            <![CDATA[select o.customer, sum(o.amount) from demo$Order o group by o.customer]]>
        </query>
        <properties>
            <property class="com.company.demo.entity.Customer" name="customer"/>
            <property datatype="decimal" name="sum"/>
        </properties>
    </valueCollectionDatasource>
</dsContext>

Value datasources are designed only for reading data, because KeyValueEntity is not persistent and cannot be saved by standard persistence mechanisms.

You can create value datasources either manually or in Studio in the Datasources tab of the Screen designer page.

ValueDatasources Studio

The Properties editor allows you to create the datasource attributes of a certain datatype and/or a Java class.

ValueDatasources Studio properties
3.6.2.4. Datasource Listeners

This is a legacy API. For new data API available since release 7.0, see Data Components.

Datasource listeners receive notifications of changes in the state of datasources and entities contained in them.

There are four types of listeners. Three of them: ItemPropertyChangeListener, ItemChangeListener and StateChangeListener are defined in the Datasource interface and can be used in any datasource. CollectionChangeListener is defined in CollectionDatasource and can be used only in datasources working with collections of entities.

Compared to ValueChangeListener, datasource listeners give finer control over the screen lifecycle and are recommended to be used with visual components bound to datasources.

Example of using datasource listeners:

public class EmployeeBrowse extends AbstractLookup {

    private Logger log = LoggerFactory.getLogger(EmployeeBrowse.class);

    @Inject
    private CollectionDatasource<Employee, UUID> employeesDs;

    @Override
    public void init(Map<String, Object> params) {
        employeesDs.addItemPropertyChangeListener(event -> {
            log.info("Property {} of {} has been changed from {} to {}",
                    event.getProperty(), event.getItem(), event.getPrevValue(), event.getValue());
        });

        employeesDs.addStateChangeListener(event -> {
            log.info("State of {} has been changed from {} to {}",
                    event.getDs(), event.getPrevState(), event.getState());
        });

        employeesDs.addItemChangeListener(event -> {
            log.info("Datasource {} item has been changed from {} to {}",
                    event.getDs(), event.getPrevItem(), event.getItem());
        });

        employeesDs.addCollectionChangeListener(event -> {
            log.info("Datasource {} content has been changed due to {}",
                    event.getDs(), event.getOperation());
        });
    }
}

The listener interfaces are described below.

  • ItemPropertyChangeListener is added by the Datasource.addItemPropertyChangeListener() method. The listener is invoked when an attribute of an entity contained in the datasource is changed. The modified entity instance itself, the name of changed attribute, old and new values can be obtained from the event object passed to the listener.

    The ItemPropertyChangeListener can be used to react to changes made in an entity instance by UI components, i.e. when a user edits input fields.

  • ItemChangeListener is added by the Datasource.addItemChangeListener() method. The listener is invoked when a selected entity instance returned by the Datasource.getItem() method is changed.

    For Datasource, it happens when another instance (or null) is set to the datasource with setItem() method.

    For CollectionDatasource, this listener is invoked when a selected element is changed in a linked visual component. For example, it may be a selected table row, tree element or item in a drop-down list.

  • StateChangeListener is added by the Datasource.addStateChangeListener() method. The listener is invoked when a state of the datasource is changed. The datasource can be in one of three states corresponding to the Datasource.State enumeration:

    • NOT_INITIALIZED – datasource has just been created.

    • INVALID – the whole DsContext, which this datasource is related to, is created.

    • VALID – datasource is ready: Datasource contains an entity instance or null, CollectionDatasource – collection of instances or an empty collection.

    Receiving a notification about changes in datasource state may be important for complex editors, which consist of several frames where it is difficult to trace the moment of setting an edited entity into the datasource. In this case, StateChangeListener can be used for the delayed initialization of certain screen elements:

    employeesDs.addStateChangeListener(event -> {
        if (event.getState() == Datasource.State.VALID)
            initDataTypeColumn();
    });
  • CollectionChangeListener is added by the CollectionDatasource.addCollectionChangeListener() method. The listener is invoked when a entity collection, which is stored in the datasource, is changed. The event object provides the getOperation() method returning value of type CollectionDatasource.Operation: REFRESH, CLEAR, ADD, REMOVE, UPDATE. It indicates the operation that caused the collection changes.

3.6.2.5. DsContext

This is a legacy API. For new data API available since release 7.0, see Data Components.

All datasources that are created declaratively are registered in the DsContext object which belongs to a screen. A reference to DsContext can be obtained using the getDsContext() method of a screen controller or via Controller Dependency Injection.

DsContext is designed for the following tasks:

  1. Organizes dependencies between datasources when navigation through a record set in one datasource (i.e. changing a "current" instance with the setItem() method) causes a related datasource to be updated. These dependencies allow you to organize master-detail relationships between visual components on screens.

    Dependencies between datasources are organized using query parameters with the ds$ prefix.

  2. Collects all changed entity instances and sends them to Middleware in a single invocation of DataManager.commit(), i.e. to save them into the database in a single transaction.

    As an example, let’s assume that some screen allows a user to edit an instance of the Order entity and a collection of OrderLine instances belonging to it. The Order instance is located in Datasource; the OrderLine collection – in nested CollectionDatasource, which is created using the Order.lines attribute.

    If a user changes some attribute of Order and creates a new instance, OrderLine, then, when a screen is committed to DataManager, two instances – changed Order and new OrderLine – will be sent simultaneously. After that, they will together be merged into one persistent context and saved into the database on the transaction commit. It allows you to not specify cascade parameters on the ORM level and avoid the problems mentioned in the @OneToMany annotation description.

    As a result of committing the transaction, DsContext receives a set of saved instances from Middleware (in the case of optimistic locking they, at least, have an increased value of the version attribute), and sets these instances in datasources replacing old ones. It allows you to work with the latest instances immediately after committing without an extra datasource refresh that produces queries to Middleware and the database.

  3. Declares two listeners: BeforeCommitListener and AfterCommitListener. They receive notifications before and after committing modified instances. BeforeCommitListener enables to supplement a collection of entities sent to DataManager to save arbitrary entities in the same transaction. A collection of saved instances that are returned from DataManager can be obtained after commit in the AfterCommitListener listener.

    This mechanism is required if some entities, with which a screen works, are not under control of datasources, but are created and changed directly in the controller code. For example, a visual component, FileUploadField, after uploading a file, creates a new entity instance, FileDescriptor, which can be saved together with other screen entities by adding to CommitContext in BeforeCommitListener.

    In the following example, a new instance of Customer will be sent to Middleware and saved to the database together with other modified screen entities when the screen is committed:

    protected Customer customer;
    
    protected void createNewCustomer() {
        customer = metadata.create(Customer.class);
        customer.setName("John Doe");
    }
    
    @Override
    public void init(Map<String, Object> params) {
        getDsContext().addBeforeCommitListener(context -> {
            if (customer != null)
                context.getCommitInstances().add(customer);
        }
    }
3.6.2.6. DataSupplier

This is a legacy API. For new data API available since release 7.0, see Data Components.

DataSupplier – interface, through which the datasources refer to Middleware for loading and saving entities. The standard implementation simply delegates to DataManager. A screen can define its implementation of the DataSupplier in dataSupplier attribute of the window element.

A reference to DataSupplier can be obtained either by injection into a screen controller or through the DsContext or Datasource instances. In both cases, an own implementation is returned if defined for the screen.

3.6.3. Dialogs and Notifications (Legacy)

This is a legacy API. For new data API available since release 7.0, see Dialogs and Notifications sections.

Dialogs and notifications can be used to display messages to users.

Dialogs have a title with a closing button and are always displayed in the center of the application main window. Notifications can be displayed both in the center and in the corner of the window, and can automatically disappear.

3.6.3.1. Dialogs

This is a legacy API. For new data API available since release 7.0, see Dialogs section.

General-purpose dialogs

General-purpose dialogs are invoked by showMessageDialog() and showOptionDialog() methods of the Frame interface. This interface is implemented by screen controller, so these methods can be invoked directly in the controller code.

  • showMessageDialog() is intended to display a message. The method has the following parameters:

    • title – dialog title.

    • message - message. For HTML type (see below), you can use HTML tags for formatting the message. When using HTML, make sure you escape data loaded from the database to avoid code injection in web client. You can use \n characters for line breaks in non-HTML messages.

    • messageType – message type. Possible types:

      • CONFIRMATION, CONFIRMATION_HTML – confirmation dialog.

      • WARNING, WARNING_HTML – warning dialog.

        The difference in message types is reflected in desktop user interface only.

        Message type can be set with parameters:

        • width - the dialog width,

        • modal - if the dialog is modal,

        • maximized - if the dialog should be maximized across the screen,

        • closeOnClickOutside - if the dialog can be closed by clicking on area outside the dialog.

          An example of showing a dialog:

          showMessageDialog("Warning", "Something is wrong", MessageType.WARNING.modal(true).closeOnClickOutside(true));
  • showOptionDialog() is intended to display a message and buttons for user actions. In addition to parameters described for showMessageDialog(), the method takes an array or a list of actions. A button is created for each dialog action. After a button is clicked, the dialog closes invoking actionPerform() method of the corresponding action.

    It is convenient to use anonymous classes derived from DialogAction for buttons with standard names and icons. Five types of actions defined by the DialogAction.Type enum are supported: OK, CANCEL, YES, NO, CLOSE. Names of corresponding buttons are extracted from the main message pack.

    Below is an example of a dialog invocation with Yes and No buttons and with a caption and messages taken from the message pack of the current screen:

    showOptionDialog(
        getMessage("confirmCopy.title"),
        getMessage("confirmCopy.msg"),
        MessageType.CONFIRMATION,
        new Action[] {
            new DialogAction(DialogAction.Type.YES, Status.PRIMARY).withHandler(e -> copySettings()),
            new DialogAction(DialogAction.Type.NO, Status.NORMAL)
        }
    );

    The Status parameter of DialogAction is used to assign a special visual style for a button representing the action. Status.PRIMARY highlights the corresponding button and makes it selected. The Status parameter can be omitted, in this case default highlighting is applied. If multiple actions with Status.PRIMARY are passed to the showOptionDialog, only the first action’s button will get the cuba-primary-action style and focus.

File upload dialog

The FileUploadDialog window provides the base functionality for loading files into the temporary storage. It contains the drop zone for drag-and-dropping files from outside of the browser and the upload button.

gui fileUploadDialog

The dialog is opened with the openWindow() method, in case of successful upload it is closed with COMMIT_ACTION_ID. You can track the close action of the dialog with CloseListener or CloseWithCommitListener and use the getFileId() and getFileName() methods to get the UUID and the name of uploaded file. Then you can create a FileDescriptor for referencing the file from the data model objects or implement any other logic.

FileUploadDialog dialog = (FileUploadDialog) openWindow("fileUploadDialog", OpenType.DIALOG);
dialog.addCloseWithCommitListener(() -> {
    UUID fileId = dialog.getFileId();
    String fileName = dialog.getFileName();

    FileDescriptor fileDescriptor = fileUploadingAPI.getFileDescriptor(fileId, fileName);
    // your logic here
});

The appearance of the Dialogs can be customized using SCSS variables with $cuba-window-modal-* prefix. You can change these variables in the visual editor after creating a theme extension or a custom theme.

3.6.3.2. Notifications

This is a legacy API. For new data API available since release 7.0, see Notifications section.

Notifications can be invoked using showNotification() method of the Frame interface. This interface is implemented by screen controller, so this method can be invoked directly from the controller code.

showNotification() method takes the following parameters:

  • caption - notification text. In case of HTML-type (see below), you can format message text using HTML-tags. When using HTML, don’t forget to escape data to prevent code injection in the web-client. You can use \n characters for line breaks in non-HTML messages.

  • description – an optional description displayed under the caption. You can also use \n character or HTML-formatting.

  • type – notification type. Possible values:

    • TRAY, TRAY_HTML - a notification is displayed in the bottom right corner of the application and disappears automatically.

    • HUMANIZED, HUMANIZED_HTML – a standard notification displayed in the center of the screen, disappears automatically.

    • WARNING, WARNING_HTML – a warning. Disappears when clicked.

    • ERROR, ERROR_HTML – a notification about an error. Disappears when clicked.

Examples of invoking a notification:

showNotification(getMessage("selectBook.text"), NotificationType.HUMANIZED);

showNotification("Validation error", "<b>Date</b> is incorrect", NotificationType.TRAY_HTML);

3.6.4. Actions over Collection (Legacy)

This is a legacy API. For new data API available since release 7.0, see List Component Actions.

For inheritors of ListComponent (Table, GroupTable, TreeTable and Tree) the set of standard actions is defined in ListActionType enumeration; their implementation classes are located in com.haulmont.cuba.gui.components.actions package.

The example of using standard actions in a table:

<table id="usersTable" width="100%">
  <actions>
      <action id="create"/>
      <action id="edit"/>
      <action id="remove"/>
      <action id="refresh"/>
  </actions>
  <buttonsPanel>
      <button action="usersTable.create"/>
      <button action="usersTable.edit"/>
      <button action="usersTable.remove"/>
      <button action="usersTable.refresh"/>
  </buttonsPanel>
  <rowsCount/>
  <columns>
      <column id="login"/>
      ...
  </columns>
  <rows datasource="usersDs"/>
</table>

These actions are described in details below.

CreateAction

CreateAction – action with create identifier. It is intended to create new entity instance and open its edit screen. If the edit screen successfully commits a new instance to the database, CreateAction adds this new instance to the table data source and makes it selected.

The following specific methods are defined in the CreateAction class:

  • setOpenType() allows you to specify new entity edit screen open mode. THIS_TAB by default.

    Since it is quite often required to open edit screens in another mode (typically, DIALOG), you can specify an openType attribute with desired value in the action element when using declarative creation of the create action. This eliminates the need to obtain action reference in the controller and set this property programmatically. For example:

    <table id="usersTable">
      <actions>
          <action id="create" openType="DIALOG"/>
  • setWindowId() allows you to specify the identifier of the entity edit screen. By default, {entity_name}.edit is used, for example sales$Customer.edit.

  • setWindowParams() allows you to set edit screen parameters passed into its init() method. The parameters can be further used directly in the datasource query via the param$ prefix or injected into the screen controller using the @WindowParam annotation.

  • setWindowParamsSupplier() is different from setWindowParams() in that it allows you to get parameter values right before the action is invoked. Supplied parameters are merged with ones set by the setWindowParams() method and can override them. For example:

    createAction.setWindowParamsSupplier(() -> {
       Customer customer = metadata.create(Customer.class);
       customer.setCategory(/* some value dependent on the current state of the screen */);
       return ParamsMap.of("customer", customer);
    });
  • setInitialValues() allows you to set initial values of attributes of the entity being created. It takes a Map object, where keys are attribute names, and values are attribute values. For example:

    Map<String, Object> values = new HashMap<>();
    values.put("type", CarType.PASSENGER);
    carCreateAction.setInitialValues(values);

    An example of setInitialValues() usage is also provided in the section of development recipes.

  • setInitialValuesSupplier() is different from setInitialValues() in that it allows you to get values right before the action is invoked. Supplied values are merged with ones set by the setInitialValues() method and can override them. For example:

    carCreateAction.setInitialValuesSupplier(() ->
        ParamsMap.of("type", /* value depends on the current state of the screen */));
  • setBeforeActionPerformedHandler() allows you to provide a handler which will be invoked by the action before its execution. The method should return true to proceed with the execution and false to abort. For example:

    customersTableCreate.setBeforeActionPerformedHandler(() -> {
        showNotification("The new customer instance will be created");
        return isValid();
    });
  • afterCommit() is invoked by the action after the new entity has been successfully committed and the edit screen has been closed. This method does not have implementation and can be overridden in inheritors to handle this event.

  • setAfterCommitHandler() allows you to provide a handler which will be called after the new entity has been successfully committed and the edit screen has been closed. This handler can be used instead of overriding afterCommit() to avoid creating the action subclass. For example:

    @Named("customersTable.create")
    private CreateAction customersTableCreate;
    
    @Override
    public void init(Map<String, Object> params) {
        customersTableCreate.setAfterCommitHandler(new CreateAction.AfterCommitHandler() {
            @Override
            public void handle(Entity entity) {
                showNotification("Committed", NotificationType.HUMANIZED);
            }
        });
    }
  • afterWindowClosed() is the last method invoked by the action after closing the edit screen regardless of whether the new entity has been committed or not. This method does not have implementation and can be overridden in inheritors to handle this event.

  • setAfterWindowClosedHandler() allows you to provide a handler which will be called after closing the edit screen regardless of whether the new entity has been committed or not. This handler can be used instead of overriding afterWindowClosed() to avoid creating the action subclass.

EditAction

EditAction is an action with edit identifier, intended to open an edit screen for a selected entity instance. If the edit screen successfully commits the instance to the database, then EditAction updates this instance in the table datasource.

The following specific methods are defined in the EditAction class:

  • setOpenType() allows you to specify entity edit screen open mode. THIS_TAB by default.

    Since it is quite often required to open edit screens in another mode (typically DIALOG), you can specify openType attribute with desired value in the action element when creating the action declaratively. This eliminates the need to obtain action reference in the controller and set this property programmatically. For example:

    <table id="usersTable">
      <actions>
          <action id="edit" openType="DIALOG"/>
  • setWindowId() allows you to specify entity edit screen identifier. {entity_name}.edit is used by default, for example, sales$Customer.edit.

  • setWindowParams() allows you to set edit screen parameters, passed to its init() method. The parameters can be further used directly in the datasource query via the param$ prefix or injected into the screen controller using the @WindowParam annotation.

  • setWindowParamsSupplier() is different from setWindowParams() in that it allows you to get parameter values right before the action is invoked. Supplied parameters are merged with ones set by the setWindowParams() method and can override them. For example:

    customersTableEdit.setWindowParamsSupplier(() ->
        ParamsMap.of("category", /* some value dependent on the current state of the screen */));
  • setBeforeActionPerformedHandler() allows you to provide a handler which will be invoked by the action before its execution. The method should return true to proceed with the execution and false to abort. For example:

    customersTableEdit.setBeforeActionPerformedHandler(() -> {
        showNotification("The customer instance will be edited");
        return isValid();
    });
  • afterCommit() is invoked by the action after the entity has been successfully committed and the edit screen has been closed. This method does not have implementation and can be overridden in inheritors to handle this event.

  • setAfterCommitHandler() allows you to provide a handler which will be called after the new entity has been successfully committed and the edit screen has been closed. This handler can be used instead of overriding afterCommit() to avoid creating the action subclass. For example:

    @Named("customersTable.edit")
    private EditAction customersTableEdit;
    
    @Override
    public void init(Map<String, Object> params) {
        customersTableEdit.setAfterCommitHandler(new EditAction.AfterCommitHandler() {
            @Override
            public void handle(Entity entity) {
                showNotification("Committed", NotificationType.HUMANIZED);
            }
        });
    }
  • afterWindowClosed() is the last method invoked by the action after closing the edit screen regardless of whether the edited entity has been committed or not. This method does not have implementation and can be overridden in inheritors to handle this event.

  • setAfterWindowClosedHandler() allows you to provide a handler which will be called after closing the edit screen regardless of whether the new entity has been committed or not. This handler can be used instead of overriding afterWindowClosed() to avoid creating the action subclass.

  • getBulkEditorIntegration() provides a possibility of bulk editing for the table records. The table should have the multiselect attribute enabled. The BulkEditor component will be opened, if multiple table lines are selected when the EditAction is called.

    The returned BulkEditorIntegration instance can be further modified by the following methods:

    • setOpenType(),

    • setExcludePropertiesRegex(),

    • setFieldValidators(),

    • setModelValidators(),

    • setAfterEditCloseHandler().

    @Named("clientsTable.edit")
    private EditAction clientsTableEdit;
    
    @Override
    public void init(Map<String, Object> params) {
        super.init(params);
    
        clientsTableEdit.getBulkEditorIntegration()
            .setEnabled(true)
            .setOpenType(WindowManager.OpenType.DIALOG);
    }

RemoveAction

RemoveAction - action with remove identifier, intended to remove a selected entity instance.

The following specific methods are defined in the RemoveAction class:

  • setAutocommit() allows you to control the moment of entity removal from the database. By default commit() method is invoked after triggering the action and removing the entity from the datasource. As result, the entity is removed from the database. You can set autocommit property into false using setAutocommit() method or corresponding parameter of the constructor. In this case you will need to explicitly invoke the datasource commit() method to confirm the removal after removing the entity from the datasource.

    The value of autocommit does not affect datasources in the Datasource.CommitMode.PARENT mode, i.e. the datasources that provide composite entities editing.

  • setConfirmationMessage() allows you to set message text for the removal confirmation dialog.

  • setConfirmationTitle() allows you to set removal confirmation dialog title.

  • setBeforeActionPerformedHandler() allows you to provide a handler which will be invoked by the action before its execution. The method should return true to proceed with the execution and false to abort. For example:

    customersTableRemove.setBeforeActionPerformedHandler(() -> {
        showNotification("The customer instance will be removed");
        return isValid();
    });
  • afterRemove() is invoked by the action after the entity has been successfully removed. This method does not have implementation and can be overridden.

  • setAfterRemoveHandler() allows you to provide a handler which will be called after the new entity has been successfully removed. This handler can be used instead of overriding afterRemove() to avoid creating the action subclass. For example:

    @Named("customersTable.remove")
    private RemoveAction customersTableRemove;
    
    @Override
    public void init(Map<String, Object> params) {
        customersTableRemove.setAfterRemoveHandler(new RemoveAction.AfterRemoveHandler() {
            @Override
            public void handle(Set removedItems) {
                showNotification("Removed", NotificationType.HUMANIZED);
            }
        });
    }

RefreshAction

RefreshAction - an action with refresh identifier. It is intended to update (reload) entities collection. When triggered, it invokes refresh() method of a datasource associated with the corresponding component.

The following specific methods are defined in the RefreshAction class:

  • setRefreshParams() allows you to set parameters passed into the CollectionDatasource.refresh() method to be used in the query. By default, no parameters are passed.

  • setRefreshParamsSupplier() is different from setRefreshParams() in that it allows you to get parameter values right before the action is invoked. Supplied parameters are merged with ones set by the setRefreshParams() method and can override them. For example:

    customersTableRefresh.setRefreshParamsSupplier(() ->
        ParamsMap.of("number", /* some value dependent on the current state of the screen */));

AddAction

AddAction – action with add identifier, intended for selecting an existing entity instance and adding it to the collection. When triggered, opens entities lookup screen.

The following specific methods are defined in the AddAction class:

  • setOpenType() allows you to specify entity selection screen open mode. THIS_TAB by default.

    Since it is often required to open the lookup screens in a different mode (usually DIALOG), the openType attribute can be specified in the action element, when creating the add action declaratively. This eliminates the need to get a reference to the action in the controller and set this property programmatically. For example:

    <table id="usersTable">
        <actions>
            <action id="add" openType="DIALOG"/>
  • setWindowId() allows you to specify entity selection screen identifier. {entity_name}.lookup by default, for example, sales$Customer.lookup. If such screen does not exist, attempts to open {entity_name}.browse screen, for example, sales$Customer.browse.

  • setWindowParams() allows you to set selection screen parameters, passed into its init() method. The parameters can be further used directly in the datasource query via the param$ prefix or injected into the screen controller using the @WindowParam annotation.

  • setWindowParamsSupplier() is different from setWindowParams() in that it allows you to get parameter values right before the action is invoked. Supplied parameters are merged with ones set by the setWindowParams() method and can override them. For example:

    tableAdd.setWindowParamsSupplier(() ->
        ParamsMap.of("customer", getItem()));
  • setHandler() allows you to set an object implementing Window.Lookup.Handler interface which will be passed to the selection screen. By default, AddAction.DefaultHandler object is used.

  • setBeforeActionPerformedHandler() allows you to provide a handler which will be invoked by the action before its execution. The method should return true to proceed with the execution and false to abort. For example:

    customersTableAdd.setBeforeActionPerformedHandler(() -> {
        notifications.create()
                .withCaption("The new customer will be added")
                .show();
        return isValid();
    });

ExcludeAction

ExcludeAction - an action with exclude identifier. It allows a user to exclude entity instances from a collection without removing them from the database. The class of this action is an inheritor of RemoveAction, however, when triggered it invokes excludeItem() of CollectionDatasource instead of removeItem(). In addition, for an entity in a nested datasource, the ExcludeAction disconnects the link with the parent entity. Therefore this action can be used for editing one-to-many associations.

The following specific methods are defined in the ExcludeAction class in addition to RemoveAction:

  • setConfirm() – flag to show the removal confirmation dialog. You can also set this property via the action constructor. By default it is set to false.

  • setBeforeActionPerformedHandler() allows you to provide a handler which will be invoked by the action before its execution. The method should return true to proceed with the execution and false to abort. For example:

    customersTableExclude.setBeforeActionPerformedHandler(() -> {
        showNotification("The selected customer will be excluded");
        return isValid();
    });

ExcelAction

ExcelAction - an action with excel identifier, intended to export table data into XLS and download the resulting file. You can add this action only to Table, GroupTable and TreeTable components.

When creating the action programmatically, you can set the display parameter with an ExportDisplay interface implementation for file download. Standard implementation is used by default.

The following specific methods are defined in the ExcelAction class:

  • setFileName() - sets the Excel file name without extension.

  • getFileName() - returns the Excel file name without extension.

  • setBeforeActionPerformedHandler() allows you to provide a handler which will be invoked by the action before its execution. The method should return true to proceed with the execution and false to abort. For example:

    customersTableExcel.setBeforeActionPerformedHandler(() -> {
        showNotification("The selected data will ve downloaded as an XLS file");
        return isValid();
    });

3.6.5. Actions of the Picker Field (Legacy)

This is a legacy API. For new data API available since release 7.0, see Picker Field Actions.

For PickerField, LookupPickerField and SearchPickerField components, a set of standard actions is defined in the PickerField.ActionType enumeration. Implementations are inner classes of the PickerField interface, which are described in details below.

The example of standard actions usage in a picker component:

<searchPickerField optionsDatasource="coloursDs"
                 datasource="carDs" property="colour">
  <actions>
      <action id="clear"/>
      <action id="lookup"/>
      <action id="open"/>
  </actions>
</searchPickerField>

LookupAction

LookupAction – action with lookup identifier, intended for selecting an entity instance and setting it as the component’s value. When triggered, it opens an entities lookup screen.

The following specific methods are defined in the LookupAction class:

  • setLookupScreenOpenType() allows you to specify entity selection screen open mode. THIS_TAB by default.

  • setLookupScreen() allows you to specify entity selection screen identifier. {entity_name}.lookup by default, for example, sales$Customer.lookup. If such screen does not exist, attempts to open {entity_name}.browse screen, for example, sales$Customer.browse.

  • setLookupScreenParams() allows you to set selection screen parameters, passed into its init() method.

  • afterSelect() is invoked by the action after the selected instance is set as the component’s value. This method does not have implementation and can be overridden.

  • afterCloseLookup() is the last method invoked by the action after closing the lookup screen regardless of whether an instance has been selected or not. This method does not have implementation and can be overridden.

ClearAction

ClearAction - an action with clear identifier, intended for clearing (i.e. for setting to null) the value of the component.

OpenAction

OpenAction - action with open identifier, intended for opening an edit screen for the entity instance which is the current value of the component.

The following specific methods are defined in the OpenAction class:

  • setEditScreenOpenType() allows you to specify entity selection screen open mode. THIS_TAB by default.

  • setEditScreen() allows you to specify entity edit screen identifier. {entity_name}.edit screen is used by default, for example, sales$Customer.edit.

  • setEditScreenParams() allows you to set edit screen parameters, passed to its `init()`method.

  • afterWindowClosed() is invoked by the action after closing the edit screen. This method does not have implementation and can be overridden in inheritors to handle this event.

3.6.6. screens.xml (Legacy)

Screens written with the new API available since v.7.0 do not need registration. They are discovered by the @UiController annotation.

Files of this type are used in the generic user interface of the Web Client for registration of screen XML-descriptors.

The file location is specified in the cuba.windowConfig application property. When you create a new project in Studio, it creates the web-screens.xml file in the root package of the web module, for example modules/web/src/com/company/sample/web-screens.xml.

The file has the following structure:

screen-config – the root element. It contains the following elements:

  1. screen – screen descriptor.

    screen attributes:

    • id – screen identifier used to reference this screen from the application code (e.g. in the Frame.openWindow() and other methods) and in the menu.xml.

    • template – path to screen’s XML-descriptor. Resources interface rules apply to loading the descriptor.

    • class – if the template attribute is not set, this attribute should contain the name of the class implementing either Callable or Runnable.

      In case of Callable, the call() method should return an instance of Window, which will be returned to the invoking code as the result of calling WindowManager.openWindow(). The class may contain a constructor with string parameters, defined by the nested param element (see below).

    • agent - if multiple templates are registered with the same id, this attribute is used to choose a template to open. There are three standard agent types: DESKTOP, TABLET, PHONE. They enable selection of a screen template depending on the current device and display parameters. See Screen Agent for details.

    • multipleOpen – optional attribute, allowing a screen to be opened multiple times. If set to false or not defined and the screen with this identifier has already been opened in the main window, the system will show the existing screen instead of opening a new one. If set to true, any number of screen instances can be opened.

    screen elements:

    • param – defines a screen parameter submitted as a map to the controller's init() method. Parameters, passed to the openWindow() methods by the invoking code, override the matching parameters set in screens.xml.

      param attributes:

      • name – parameter name.

      • value – parameter value. Strings true and false are converted into the corresponding Boolean values.

  2. include – includes a different file, e.g. screens.xml.

    include attributes:

    • file – path to a file according to the rules of the Resources interface.

Example of a screens.xml file:

<screen-config xmlns="http://schemas.haulmont.com/cuba/screens.xsd">

  <screen id="sales$Customer.lookup" template="/com/sample/sales/gui/customer/customer-browse.xml"/>
  <screen id="sales$Customer.edit" template="/com/sample/sales/gui/customer/customer-edit.xml"/>

  <screen id="sales$Order.lookup" template="/com/sample/sales/gui/order/order-browse.xml"/>
  <screen id="sales$Order.edit" template="/com/sample/sales/gui/order/order-edit.xml"/>

</screen-config>

3.7. Frontend User Interface

The Frontend UI client block provides an ability to quickly bootstrap customer facing portals with mobile-first responsive web UI.

npm package @cuba-platform/front-generator is used for creation of front clients and their screens.

3.7.1. Adding Frontend UI using Studio

In order to add the frontend client module to your project:

  • Open CUBA project tree in CUBA Studio;

  • Right-click on the Modules node;

  • Select Manage modules > Create 'front' module item.

Studio will install frontend generator in modules/front/generation folder (it may take a while). Then you will be prompted to choose what preset you want to use for your frontend: React or Polymer (deprecated).

3.7.2. React-based User Interface

Documentation can be found in the README file of the generator source code repository.

3.7.3. Polymer-based User Interface (Deprecated)

Starting from the version 7.2 of the Platform the Polymer UI is deprecated. Use React UI.

It is based on Polymer library and enables tight integration with mobile browsers for adding web applications to the device home screen and for offline work.

The CUBA platform Polymer UI has the following features:

  • The Polymer build system is fully integrated into the project build system based on Gradle, so all building tools are downloaded and installed automatically. At the same time, once the Polymer module is created in the project, it can be developed further by front-end developers separately using the standard Polymer toolchain.

  • The platform provides a set of web components for working with the middleware through the standard CUBA REST API. The components are described below.

  • CUBA Studio enables easy creation of the Polymer client module and scaffolding of the application web components around the data model and middleware services of the project. Studio contains an extendable set of templates for creation of application level components.

Our current approach is to stick to Polymer’s techniques and tools which are used for creating Progressive Web Apps. The aim is to follow Polymer’s guidelines and best practices to provide similar learning curve and development experience. Polymer applications have component-based architecture and consist of Web Components.

In order to start developing effectively, you need to be familiar with basic Polymer concepts. Here is a very quick intro to the Polymer: https://github.com/Polymer/polymer#polymer-in-1-minute. However, it’s better to learn it more in depth: https://polymer-library.polymer-project.org/2.0/start/. Since the Polymer is built around the standards by learning it in most cases you learn the web platform itself.

3.7.3.1. Requirements

Git should be installed and available from the command line. It’s required for bower - the package manager for front-end applications.

3.7.3.2. Supported Browsers

See the list of supported browsers on the Polymer’s website

Please do not to use Polymer client if you are targeting legacy browsers.

3.7.3.3. Build System and Project Structure

The following tools are used in Polymer client build chain:

By default Gradle handles installation and invocation of these tools. It’s possible to use them directly, see Using Native Polymer Tools.

Polymer 2.x and its native elements are written using ES6, thus it requires an additional build step (ES6 → ES5 compilation) in order to support old browsers.

The default preset used in Polymer client is es6-unbundled. It means that if you are targeting old browsers and production deployment you should change it to es5-bundled.

In order to change build preset open polymer.json and change builds property accordingly e.g.:

  "builds": [
    {
      "preset": "es5-bundled",
      "basePath": "/app-front/",
      "addServiceWorker": false
    }
  ]

You can specify several presets or customize build process in polymer.json. More info about presets and options is available on Polymer website.

In order the results of specific preset build being deployed to Tomcat you need to change deploy task in build.gradle:

    task deploy(type: Copy, dependsOn: [assemble, deployUnbundled]) {
        from file('build/es5-unbundled')
        into "$cuba.tomcat.dir/webapps/$frontAppDir"
    }

Notice es6-bundledes5-unbundled change in polymer.json and build.gradle.

3.7.3.3.1. Directory Structure
front/
|-- src/
|   |-- app-shell.html
|   |-- shared-styles.html
|-- images
|   |-- app-icon/
|   |-- favicon.ico
|-- .gitignore
|-- bower.json
|-- index.html
|-- manifest.json
|-- package.json
|-- polymer.json
|-- service-worker.js
|-- sw-precache-config.js
src

Folder where components are placed.

package.json

Lists dependencies on Node.js modules which will be used in a build purposes.

bower.json

Lists dependencies on web libraries (primarily web components) which will be used at runtime.

polymer.json

Polymer build configuration.

index.html

An application entry point. Contains logic on loading polyfills and <appname>-shell.html import.

manifest.json

Web app manifest. Contains information which used when the application is being added to a device’s home screen. More info: https://developer.mozilla.org/en-US/docs/Web/Manifest

service-worker.js

Service worker stub.

sw-precache-config.js

Config used by sw-precache library in order to generate service worker at build time (disabled by default). See Offline Capabilities.

3.7.3.3.2. Hot Deploy

When you run and deploy your application using CUBA Studio or Gradle the build system will bundle your components according to the configuration in polymer.json file. By default, it will bundle the whole application into a single app-shell.html file. When you change some of your app components Studio will hot deploy it to the tomcat. Also, it will replace bundled app-shell.html with an unbundled version in order changes to be picked. Keep it in mind if you deploy your application on production directly from tomcat/webapps.

If you use es5-bundled build preset, hot deploy will not work as expected since Studio does not perform any JavaScript transpilation on the fly.

If you use TypeScript based client you have to run npm run watch command manually to hot-deploy changes in components classes.

3.7.3.3.3. Using Native Polymer Tools

You can use native Polymer framework toolchain when developing Polymer UI components. It can be convenient if a separate team of front-end developers works on the project. In this case, Node.js should be installed on the system.

Install bower and polymer-cli globally:

npm install bower polymer-cli -g

Then you can build and run the web application without Gradle:

cd modules/front
npm install
bower install
polymer serve

You need to specify the absolute path to REST API in modules/front/index.html if you want to serve the app by polymer server (instead of Tomcat), e.g.:

<myapp-shell api-url="http://localhost:8080/app/rest/"></myapp-shell>

After that, the web application will be available at http://localhost:8081 (see the particular port in command line output) and it will work with the REST API running at http://localhost:8080/app/rest/.

3.7.3.4. CUBA Web Components

The detailed API reference of CUBA elements can be found here.

3.7.3.4.1. Initialization

In order to use any cuba- element you need to initialize common library and connection to the REST API using cuba-app element :

<cuba-app api-url="/app/rest/"></cuba-app>

It should be placed once in your app as early as possible. Do not change properties dynamically or detach/attach the element after initialization.

3.7.3.4.2. Working With Data

In order to load data just place some of cuba-data elements in HTML and specify required attributes.

Entities Loading

Use cuba-entities to load entities. Once entity-name and view attributes are specified the element loads list of entities and exposes it to the Polymer data binding via data property:

<cuba-entities entity-name="sec$User" view="_local" data="{{users}}"></cuba-entities>

Then you can display the data as simple as:

<template is="dom-repeat" items="[[users]]" as="user">
  <div>[[user.login]]</div>
</template>

Entities Querying

Define a query as described here.

Use cuba-query element to retrieve query results. You can optionally pass parameters using params property:

<cuba-query id="query"
            auto="[[auto]]"
            entity-name="sec$User"
            query-name="usersByName"
            data="{{users}}">
</cuba-query>

<template is="dom-repeat" items="[[users]]" as="user">
  <div>[[user.login]]</div>
</template>

Service Invocation

Expose a service and it’s method as described here. Use cuba-service element to invoke the method:

<cuba-service service-name="cuba_ServerInfoService"
              method="getReleaseNumber"
              data="{{releaseNumber}}"
              handle-as="text"></cuba-service>

Release number: [[releaseNumber]]

Entity Creation

cuba-entity-form and cuba-service-form elements facilitate sending data to the backend.

In the example below, we bind user object which should be persisted to the entity property.

<cuba-entity-form id="entityForm"
                  entity-name="sec$User"
                  entity="[[user]]"
                  on-cuba-form-response="_handleFormResponse"
                  on-cuba-form-error="_handleFormError">

  <label>Login: <input type="text" name="login" value="{{user.login::input}}"></label>
  <label>Name: <input type="text" name="login" value="{{user.name::input}}"></label>

  <button on-tap="_submit">Submit</button>

</cuba-entity-form>

<paper-toast id="successToast">Entity created</paper-toast>
<paper-toast id="errorToast">Entity creation error</paper-toast>
_submit: function() {
  this.$.entityForm.submit();
},
_handleFormResponse: function() {
  this.user = getUserStub();
  this.$.successToast.open();
},
_handleFormError: function() {
  this.$.errorToast.open();
}

You should enable anonymous access in the REST API if you want to use the examples above without forcing users to log in.

3.7.3.5. Styling

See the Polymer’s styling guide. The most noticeable difference between traditional approach is how global styles are specified. Since Polymer elements use Shadow DOM global styles do not leak inside the components. You need to use style-modules instead. There is a shares-styles.html file in Polymer client which is automatically being imported to any new component created in Studio.

3.7.3.6. Offline Capabilities

Experimental!

The technologies listed below are not supported by all browsers yet (e.g. service workers are not yet implemented in Safari).

Currently, together with the Polymer we offer to use Progressive Web Applications techniques such as web app manifest 2 to have native-like presence on the user’s home screen. See the manifest.json file in Polymer client module.

There are two main approaches:

  • Service Workers which primarily used to cache the app itself. Take a look at sw-precache-config.js file generated with Polymer client. In order to enable service worker generation set "addServiceWorker": true in polymer.json.

More info on how to setup and use service workers can be found here.

3.7.3.7. TypeScript Support

Since Release 6.9 you can scaffold TypeScript based Polymer client in Studio. When creating Polymer client module you will be able to select polymer2-typescript preset of the client. Main differences with basic JavaScript version:

Component classes are placed in separate *.ts files
myapp-component.ts
namespace myapp {

  const {customElement} = Polymer.decorators;

  @customElement('myapp-component')
  class MyappComponent extends Polymer.Element {
  }
}
myapp-component.html
<link rel="import" href="../bower_components/polymer/polymer.html">

<link rel="import" href="./shared-styles.html">

<dom-module id="myapp-component">
  <template>
     <!-- some html markup -->
  </template>
  <script src="myapp-component.js"></script>
</dom-module>
There is an additional phase of build process - TypeScript compilation

See scripts section of package.json

{
  "scripts": {
    "build": "npm run compile && polymer build",
    "compile": "tsc",
    "watch": "tsc -w"
  }
}

Now before polymer build there is npm run compile command which effectively runs TypeScript compilation (tsc).

If you change the source code of component classes and expect your changes to be picked by Studio’s hot deploy you should manually run npm run watch command in modules/front directory.

3.7.3.7.1. Create Polymer Components with TypeScript

TypeScript decorators by Polymer team provide more convenient and less verbose way to create component classes. Let’s look at the following example:

/// <reference path="../bower_components/cuba-app/cuba-app.d.ts" />
/// <reference path="../bower_components/app-layout/app-drawer/app-drawer.d.ts" />
/// <reference path="../bower_components/app-layout/app-drawer-layout/app-drawer-layout.d.ts" />

namespace myapp {

  // Create shortcuts to decorators
  const {customElement, property, observe, query} = Polymer.decorators;

  @customElement('myapp-component')
  class MyappComponent extends (Polymer.mixinBehaviors([CubaAppAwareBehavior, CubaLocalizeBehavior], Polymer.Element) as
    new () => Polymer.Element & CubaAppAwareBehavior & CubaLocalizeBehavior) {

    @property({type: Boolean})
    enabled: boolean;

    @property({type: String})
    caption: string;

    @query('#drawer')
    drawer: AppDrawerElement;

    @observe('app')
    _init(app: cuba.CubaApp) {
      ...
    }

    @computed('enabled', 'caption')
    get enabledCaption() {
      ...
    }
  }
}
  • /// <reference path="…​"/> - allows you to import TypeScript declarations of other elements or libraries.

  • @customElements('element-name') decorator eliminates necessity to define static get is() method and manual invocation of customElements.define().

  • @property() decorator allows you to specify component’s properties.

  • @query('.css-selector') decorator allows you to select DOM elements of your component.

  • @observe('propertyName') decorator allows you to define property observers.

  • @computed() decorator allows you to define computed methods.

See polymer-decorators github repository for more examples.

3.7.3.8. Troubleshooting
Proxy

If you work behind a proxy you may need to configure bower and npm accordingly. In order to allow bower and npm to work behind a proxy create the following files in the modules/front/ directory:

.bowerrc
{
    "proxy":"http://<user>:<password>@<host>:<port>",
    "https-proxy":"http://<user>:<password>@<host>:<port>"
}
.npmrc
proxy=http://<user>:<password>@<host>:<port>
https-proxy=http://<user>:<password>@<host>:<port>
NPM install failed

There is a known issue with npm install command on Windows.

You may experience the following error in the build process:

npm ERR! code EPERM
npm ERR! errno -4048
npm ERR! syscall rename
npm ERR! Error: EPERM: operation not permitted,

As a workaround, you can try to disable Windows Defender/any other antivirus software, make sure you do not open project in any IDE and run build again.

Track the following issue for upcoming possible solution.

3.8. Portal Components

In this manual, a portal is a client block, which is designed to:

  • provide an alternative web-interface, which is usually intended for users outside of the organization;

  • provide an interface for integration with mobile applications and third-party systems.

A specific application may contain several portal modules intended for different purposes; for example, in an application, which automates business tasks, it can be a public web site for customers, an integration module for a mobile application for ordering a taxi, an integration module for a mobile application for drivers, etc.

The cuba application component includes the portal module, which is a template to create portals in projects. It provides basic functionality of the client block to work with Middleware. Besides, the universal REST API, included to the portal module as a dependency, is turned on by default.

Below is an overview of the main components provided by the platform in the portal module.

  • PortalAppContextLoader – the AppContext loader; must be registered in the listener element of the web.xml file.

  • PortalDispatcherServlet – the central servlet that distributes requests to Spring MVC controllers, for both the web interface and REST API. The set of Spring context configuration files is defined by the cuba.dispatcherSpringContextConfig application property. This servlet must be registered in web.xml and mapped to the root URL of the web application.

  • App – the object that contains information on the current HTTP request and the reference to Connection object. The App instance can be obtained in the application code by calling the App.getInstance() static method.

  • Connection – allows a user to log in/out of the Middleware.

  • PortalSession – the object of a user session that is specific for the portal. It is returned by the UserSessionSource infrastructure interface and by the PortalSessionProvider.getUserSession() static method.

    It has an additional isAuthenticated() method, which returns true if this session belongs to a non-anonymous user, i.e. a user explicitly registered with the login and password.

    When a user first accesses the portal, the SecurityContextHandlerInterceptor creates an anonymous session for him (or ties to an already existing one) by registering at Middleware with a user name specified in the cuba.portal.anonymousUserLogin application property. The registration is made by loginTrusted() method, so it is necessary to set the cuba.trustedClientPassword property in the portal block as well. Thus, any anonymous user of the portal can work with Middleware with cuba.portal.anonymousUserLogin user rights.

    If the portal contains user registration page with name and password SecurityContextHandlerInterceptor assigns the session of the explicitly registered user to the execution thread after Connection.login() is executed, and the work with Middleware is performed on this user’s behalf.

  • PortalLogoutHandler – handles the navigation to the logout page. It must be registered in the portal-security-spring.xml project file.

3.9. Platform Features

This section provides overview on various optional features provided by the platform.

3.9.1. Dynamic Attributes

Dynamic attributes are additional entity attributes, that can be added without changing the database schema and restarting the application. Dynamic attributes are usually used to define new entity properties at deployment or production stage.

CUBA dynamic attributes implement the Entity-Attribute-Value model.

dynamic attributes
Figure 37. Dynamic Attributes Classes Diagram
  • Category - defines a category of objects and the corresponding set of dynamic attributes. The category must be assigned to some entity type.

    For example, there is an entity of the Car type. We can define two categories for it: Truck and Passenger. The Truck category will contain Load Capacity and Body Type attributes, and the Passenger category – Number of Seats and Child Seat.

  • CategoryAttribute - defines a dynamic attribute related to some category. Each attribute describes a single field of a definite type. The required Code field contains the system name of the attribute. The Name field contains the human-readable attribute name.

  • CategoryAttributeValue - dynamic attribute value for a particular entity instance. Dynamic attribute values are physically stored in the dedicated SYS_ATTR_VALUE table. Each table record has a reference to some entity (ENTITY_ID column).

An entity instance can have dynamic attributes of all categories related to the entity type. So if you create two categories of the Car entity mentioned above, you will be able to specify any dynamic attribute from both categories for a Car instance. If you want to be able to classify an entity instance as belonging to a single category (a car can be either truck or passenger), the entity must implement Categorized interface. In this case an entity instance will have the reference to a category, and dynamic attributes from this category only.

Loading and saving of dynamic attribute values is handled by DataManager. The setLoadDynamicAttributes() method of LoadContext and the dynamicAttributes() method of the fluent API are used to indicate that dynamic attributes should be loaded for entity instances. By default, dynamic attributes are not loaded. At the same time, DataManager always saves dynamic attributes contained in entity instances passed to commit().

Dynamic attribute values are available through getValue() / setValue() methods for any persistent entity inherited from BaseGenericIdEntity. An attribute code with the + prefix should be passed to these methods, for example:

Car entity = dataManager.load(Car.class).id(carId).dynamicAttributes(true).one;

Double capacity = entity.getValue("+loadCapacity");
entity.setValue("+loadCapacity", capacity + 10);

dataManager.commit(entity);

In fact, the direct access to attribute values in the application code is rarely needed. Any dynamic attribute can be automatically displayed in any Table or Form component bound to a data container with the entity for which the dynamic attribute was created. The attribute editor described in the next section allows you to specify screens and components that should show the attribute.

User permissions to access dynamic attributes can be set in the security role editor in the same way as for regular attributes. Dynamic attributes are displayed with the + prefix.

3.9.1.1. Managing Dynamic Attributes

You can manage dynamic attributes in the Administration > Dynamic Attributes screen. The screen has a list of categories on the left and attributes belonging to the selected category on the right.

In order to create a dynamic attribute for an entity, first create a category for it. The Default checkbox in the category editor indicates that this category will be automatically selected for a new instance if the entity implements the Categorized interface. If the entity does not implement Categorized, the checkbox value is not used and you can create either a single category for the entity, or multiple categories - all their attributes will be displayed according to the visibility settings.

After making changes in the dynamic attributes configuration, click Apply settings button in the categories browser. Changes can also be applied via Administration > JMX Console by calling the clearDynamicAttributesCache() method of the app-core.cuba:type=CachingFacade JMX bean.

Below is an example of the category editor screen:

categoryEditor
Figure 38. Category Editor

The Name localization groupbox is shown on the category editor screen if the application supports more than one language. It enables setting the localized values of category names for each available locale.

categoryLocalization
Figure 39. Category name localization

On the Attributes Location tab, you can set the location of each dynamic attribute inside the DynamicAttributesPanel.

dynamic attributes location
Figure 40. Setting the location of attributes

Specify the number of columns in the Columns count drop-down list. To change the position of an attribute, drag it from the attribute list to the desired column and the desired line. You can add empty cells or change the order of the attributes. After making the changes, click on the Save configuration button.

Location of the attributes in the DynamicAttributesPanel of the entity editor:

dynamic attributes location rezult

Dynamic attribute editor enables setting the name, system code, description, value type, the default value of the attribute and the validation script.

runtimePropertyEditor
Figure 41. Dynamic Attribute Editor

For all value types, except Boolean, there is a Width field available to set up the field width in Form in pixels or as a percentage. If the Width field is empty, its assumed value is 100%.

For all value types, except Boolean, there is also an Is collection checkbox available. It allows you to create multi-valued dynamic attributes of a selected type.

For all numeric value types: Double, Fixed-point number, Integer – the following fields are available:

  • Minimum value – when you enter an attribute value, it checks that the value must be greater than or equal to the specified minimum value.

  • Maximum value – when you enter an attribute value, it checks that the value must be less than or equal to the specified maximum value.

For the Fixed-point number value type, the Number format pattern field is available in which you can specify a format pattern. The pattern is set according to the rules described in DecimalFormat.

For all value types, you can specify a script in the Validation script field to validate the value entered by the user. The validation logic is set in the Groovy script. If Groovy validation fails, the script should return an error message. Otherwise, the script should return nothing or may return null. The checked value is available in the script in the value variable. The error message uses a Groovy string; the $value key can be used in the message to generate the result.

For example:

if (!value.startsWith("correctValue")) return "the value '\$value' is incorrect"

For the Enumeration value type, the set of named values is defined in the Enumeration field via the list editor.

runtimePropertyEnum
Figure 42. Dynamic Attribute Editor for Enumeration type

Each enumeration value can be localized to the languages, available for the application.

runtimePropertyEnumLocalization
Figure 43. Dynamic Attribute Localization for Enumeration type

For String, Double, Entity, Fixed-point number and Integer data types the checkbox Lookup field is available. If this checkbox is set, the user can select the attribute value from the drop-down list. The list of valid values can be configured on the Calculated values and options tab. For the Entity data type Where and Join clauses are configured.

Consider the Calculated values and options tab. In the Attribute depends on field, you can specify which attributes the current attribute depends on. When changing the value of one of these attributes, either the script for calculating the attribute value or the script for calculating the list of possible values will be recalculated.

The Groovy script for calculating the attribute value is specified in the Recalculation value script field. The script must return a new parameter value. The following variables are passed to the script:

  • entity – the edited entity;

  • dynamicAttributes – a map where the key – the attribute code, value – the value of the dynamic attribute.

dynamic attributes recalculation
Figure 44. Recalculation value script

Example of a recalculation script using the dynamicAttributes map:

if (dynamicAttributes['PassengerNumberofseats'] > 9) return 'Bus' else return 'Passenger car'

The script will be evaluated each time the value of one of the attributes which the attribute depends on is changed.

If the script is defined, the attribute input field will be non-editable.

Recalculation works only with the following UI components: Form, DynamicAttributesPanel.

The Options type field determines the type of the options loader and is required if the Lookup field checkbox has been set on the General tab. If the checkbox is not set, the Options type field is unavailable.

Available option loader types: Groovy, SQL, JPQL (only for Entity data type).

  • The Groovy options loader loads a list of values using the Groovy script. The entity variable is passed to the script where you can access the attributes of the entity (including dynamic attributes). An example script for an attribute of type String:

    dynamic attributes Groovy options
    Figure 45. The script for the Groovy options loader
  • The SQL options loader loads a list of values using a SQL script. You can access the entity id using the ${entity} variable. To access entity parameters, use the ${entity.<field>} construction, where field is the name of the entity parameter. The + prefix is used to access the dynamic attributes of an entity, such as ${entity.+<field>}. Script sample (here we access to the entity and dynamic attribute Categorytype):

    select name from DYNAMICATTRIBUTESLOADER_TAG
    where CUSTOMER_ID = ${entity}
    and NAME = ${entity.+Categorytype}
  • The JPQL option loader applies only to a dynamic attribute of the Entity type. JPQL conditions are specified in the JoinClause and Where Clause fields. Also, you can use the Constraint Wizard, which enables the visual creation of the JPQL conditions. You can use {entity} and {entity.<field>} variables in JPQL parameters.

Localization is supported for all types of dynamic attributes:

runtimePropertyLocalization
Figure 46. Dynamic Attribute Localization
Visibility of attributes

A dynamic attribute also has visibility settings which define the screens where it should be displayed. By default, the attribute is not shown.

runtimePropertyVisibility
Figure 47. Dynamic Attribute Visibility Settings

In addition to the screen, you can also specify a component in which the attribute should appear (for example, for screens where several Form components show the fields of the same entity).

If the attribute is marked as visible on a screen, it will automatically appear in all forms and tables displaying entities of the corresponding type on the screen.

Access to dynamic attributes can also be restricted by user role settings. Security settings for dynamic attributes are similar to those for regular attributes.

Dynamic attributes can be added to a screen manually. In order to do this, add dynamicAttributes="true" attribute to the data loader and use attribute’s code with + prefix when binding UI components to it:

<data>
    <instance id="carDc" class="com.company.app.entity.Car" view="_local">
        <loader id="carDl" dynamicAttributes="true"/>
    </instance>
</data>
<layout>
    <form id="form"
          dataContainer="carDc">
        <!--...-->
        <textField property="+PassengerNumberofseats"/>
    </form>
3.9.1.2. DynamicAttributesPanel

If an entity implements com.haulmont.cuba.core.entity.Categorized interface, you can use DynamicAttributesPanel component for displaying dynamic attributes of this entity. This component allows a user to select a category for the particular entity instance and specify values of dynamic attributes of this category.

In order to use the DynamicAttributesPanel component in an edit screen, do the following:

  • Include the category attribute to the view of your categorized entity:

    <view entity="ref_Car" name="car-view" extends="_local">
        <property name="category" view="_minimal"/>
    </view>
  • Declare an InstanceContainer in the data section:

    <data>
       <instance id="carDc"
                 class="com.company.ref.entity.Car"
                 view="car-view">
          <loader dynamicAttributes="true"/>
       </instance>
    </data>

    Set the dynamicAttributes parameter of the loader to true to load the entity’s dynamic attributes. Dynamic attributes are not loaded by default.

  • Now, the dynamicAttributesPanel visual component may be included in the XML-descriptor of the screen:

    <dynamicAttributesPanel dataContainer="carDc"
                            cols="2"
                            rows="2"
                            width="AUTO"/>

    You can specify the number of columns to display dynamic attributes using the cols parameter. Or you can use the rows parameter to specify the number of rows (in this case, the number of columns will be calculated automatically). By default, all attributes will be displayed in one column.

    On the Attributes Location tab of the category editor, you can more flexibly customize the position of the dynamic attributes. In this case, the values of the cols and rows parameters will be ignored.

3.9.2. Email Sending

The platform provides email sending facilities with the following features:

  • Synchronous or asynchronous sending. In case of synchronous sending, the calling code waits till the message is sent to the SMTP server. In case of asynchronous sending, the message is persisted to the database and the control is returned immediately to the calling code. The actual sending is done later by a scheduled task.

  • Reliable tracking of message sending timestamp or errors in the database for both synchronous and asynchronous modes.

  • User interface to search and view information about sent messages, including all message attributes and content, sending status and the number of attempts.

3.9.2.1. Sending Methods

To send an email, the EmailerAPI bean should be used at the Middleware, and the EmailService service – at the client tier.

The basic methods of these components are described below:

  • sendEmail() – synchronous message sending. The calling code is blocked while sending the message to the SMTP server.

    The message can be transmitted in the form of a set of parameters (the comma-separated list of recipients, subject, content, array of attachments), and in the form of a special EmailInfo object, which encapsulates all this information and allows you to explicitly set the sender’s address and to form the message body using a FreeMarker template.

    EmailException may be thrown during synchronous sending, containing the information on the recipient addresses, where delivery has failed, and the corresponding error messages.

    During the execution of the method, a SendingMessage instance is created in the database for each recipient. It has the initial SendingStatus.SENDING status, and SendingStatus.SENT after successful sending. In case of a message sending error, the message status changes to SendingStatus.NOTSENT.

  • sendEmailAsync() – asynchronous message sending. This method returns the list (by the number of recipients) of SendingMessage instances in SendingStatus.QUEUE status, which were created in the database. The actual sending is performed with the subsequent call of the EmailerAPI.processQueuedEmails() method, which should be invoked from a scheduled task with the desired frequency.

3.9.2.2. Email Attachments

The EmailAttachment object is a wrapper that holds the attachment as a byte array (the data field), the file name (the name field), and, if necessary, the attachment identifier which is unique for this message (the optional but useful contentId field).

The attachment identifier may be used to insert images in the message body. For this, a unique contentId (for example, myPic) is specified when creating EmailAttachment. Expression like cid:myPic can be used as a path to insert the attachment in the message body. So, to insert an image you can specify the following HTML element:

<img src="cid:myPic"/>
3.9.2.3. Configuring Email Sending Parameters

Email sending parameters can be configured using the application properties listed below. All of them are runtime parameters and are stored in the database, but can be overridden for a specific Middleware block in its app.properties file.

All email sending parameters are available via the EmailerConfig configuration interface.

  • cuba.email.fromAddress – the default sender’s address. It is used if the EmailInfo.from attribute is not specified.

    Default value: DoNotReply@localhost

  • cuba.email.smtpHost – the address of the SMTP server.

    Default value: test.host

  • cuba.email.smtpPort – the port of the SMTP server.

    Default value: 25

  • cuba.email.smtpAuthRequired flags whether the SMTP server requires authentication. It corresponds to the mail.smtp.auth parameter, which is passed at the creation of the javax.mail.Session object.

    Default value: false

  • cuba.email.smtpSslEnabled flags whether SSL protocol is enabled. It corresponds to the mail.transport.protocol parameter with the smtps value, which is passed at the creation of the javax.mail.Session object.

    Default value: false

  • cuba.email.smtpStarttlsEnable – flags the use of the STARTTLS command when authenticating on the SMTP server. It corresponds to the mail.smtp.starttls.enable parameter, which is passed at the creation of the javax.mail.Session object.

    Default value: false

  • cuba.email.smtpUser – the user name for SMTP server authentication.

  • cuba.email.smtpPassword – the user password for SMTP server authentication.

  • cuba.email.delayCallCount – is used in asynchronous sending of emails to skip first few calls of EmailManager.queueEmailsToSend() after server startup to reduce the load during application initialization. Email sending will start with the next call.

    Default value: 2

  • cuba.email.messageQueueCapacity – for asynchronous sending, the maximum number of messages read from the queue and sent in one call of EmailManager.queueEmailsToSend().

    Default value: 100

  • cuba.email.defaultSendingAttemptsCount for asynchronous sending, the default number of attempts to send an email. It is used if the attemptsCount parameter is not specified when calling Emailer.sendEmailAsync().

    Default value: 10

  • cuba.email.maxSendingTimeSec – the maximum expected time in seconds, which is required to send an email to the SMTP server. It is used for asynchronous sending to optimize the selection of SendingMessage objects from the DB queue.

    Default value: 120

  • cuba.email.sendAllToAdmin – indicates that all messages should be sent to the cuba.email.adminAddress address, regardless of the specified recipient’s address. It is recommended to use this parameter during system development and debugging.

    Default value: false

  • cuba.email.adminAddress – the address, to which all messages are sent if the cuba.email.sendAllToAdmin property is switched on.

    Default value: admin@localhost

  • cuba.emailerUserLogin – the login of system user, used by asynchronous email sending code to be able to persist the information to the database. It is recommended to create a separate user (for example, emailer) without a password, so that it will be impossible to log in under their name via user interface. This is also convenient to search for messages related to email sending in the server log.

    Default value: admin

  • cuba.email.exceptionReportEmailTemplateBody - path to the exception report email body *.gsp-template location.

    The templates are based on Groovy SimpleTemplateEngine syntax, thus you can use Groovy blocks inside of the template content:

    • toHtml() method converts the string to HTML string by escaping and replacing special symbols,

    • timestamp - last attempt date to send the email,

    • errorMessage - the message of error,

    • stacktrace - stack trace of the error,

    • user - a reference to the User object.

    Example of a template file:

    <html>
    <body>
    <p>${timestamp}</p>
    <p>${toHtml(errorMessage)}</p>
    <p>${toHtml(stacktrace)}</p>
    <p>User login: ${user.getLogin()}</p>
    </body>
    </html>
  • cuba.email.exceptionReportEmailTemplateSubject - path to the exception report email subject *.gsp template location.

    Example of a template file:

    [${systemId}] [${userLogin}] Exception Report

You can also use properties from JavaMail API, adding them to the app.properties file of the core module. The mail.* properties are passed at the creation of the javax.mail.Session object.

You can view the current parameter values and send a test message using the app-core.cuba:type=Emailer JMX bean.

3.9.2.4. Email Sending Guide

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

Let’s 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:

    @UiController("sample_NewsItem.edit")
    @UiDescriptor("news-item-edit.xml")
    @EditedEntityContainer("newsItemDc")
    @LoadDataBeforeShow
    public class NewsItemEdit extends StandardEditor<NewsItem> {
    
        private boolean justCreated; (1)
    
        @Inject
        protected EmailService emailService;
    
        @Inject
        protected Dialogs dialogs;
    
        @Subscribe
        public void onInitEntity(InitEntityEvent<NewsItem> event) { (2)
            justCreated = true;
        }
    
        @Subscribe(target = Target.DATA_CONTEXT)
        public void onPostCommit(DataContext.PostCommitEvent event) { (3)
            if (justCreated) {
                dialogs.createOptionDialog() (4)
                        .withCaption("Email")
                        .withMessage("Send the news item by email?")
                        .withType(Dialogs.MessageType.CONFIRMATION)
                        .withActions(
                                new DialogAction(DialogAction.Type.YES) {
                                    @Override
                                    public void actionPerform(Component component) {
                                        sendByEmail();
                                    }
                                },
                                new DialogAction(DialogAction.Type.NO)
                        )
                        .show();
            }
        }
    
        private void sendByEmail() { (5)
            NewsItem newsItem = getEditedEntity();
            EmailInfo emailInfo = new EmailInfo(
                    "john.doe@company.com,jane.roe@company.com", (6)
                    newsItem.getCaption(), (7)
                    null, (8)
                    "com/company/demo/templates/news_item.txt", (9)
                    Collections.singletonMap("newsItem", newsItem) (10)
            );
            emailService.sendEmailAsync(emailInfo);
        }
    }
    1 - indicates that a new item was created in this editor.
    2 - this method is invoked when a new item is initialized.
    3 - this method is invoked after commit of data context.
    4 - if a new entity was saved to the database, ask a user about sending an email.
    5 - queues an email for sending asynchronously.
    6 - recipients.
    7 - subject.
    8 - the from address will be taken from the cuba.email.fromAddress application property.
    9 - body template path.
    10 - template parameters.

    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.

3.9.3. Entity Inspector

The entity inspector enables working with any application objects without having to create dedicated screens. The inspector dynamically generates the screens to browse and edit the instances of the selected entity.

This gives the system administrator an opportunity to review and edit the data that is not accessible from standard screens due to their design, and to create the data model and main menu sections linked to the entity inspector only, at prototyping stage.

The entry point for the inspector is the com/haulmont/cuba/gui/app/core/entityinspector/entity-inspector-browse.xml screen.

If a String-type parameter named entity with an entity name has been passed to the screen, the inspector will show a list of entities with the abilities for filtering, selection and editing. The parameter can be specified when registering the screen in screens.xml, for example:

screens.xml

<screen id="sales$Product.lookup"
      template="/com/haulmont/cuba/gui/app/core/entityinspector/entity-inspector-browse.xml">
  <param name="entity"
         value="sales$Product"/>
</screen>

menu.xml

<item id="sales$Product.lookup"/>

Screen identifier defined as {entity_name}.lookup allows PickerField and LookupPickerField components to use this screen within the PickerField.LookupAction standard action.

Generally, the screen may be called without any parameters. In this case, the top part will contain an entity selection field. In the cuba application component, the inspector screen is registered with the entityInspector.browse identifier, so it can be simply referenced in a menu item:

<item id="entityInspector.browse"/>

3.9.4. Entity Log

This mechanism tracks entity persistence at the entity listeners level, i.e. it is guaranteed to track all changes passing through persistent context of the EntityManager. Direct changes to entities in the database using SQL, including the ones performed using NativeQuery or QueryRunner, are not tracked.

Modified entity instances are passed to registerCreate(), registerModify() and registerDelete() methods of the EntityLogAPI bean before they are saved to the database. Each method has auto parameter, allowing separation of automatic logs added by entity listeners from manual logs added by calling these methods from the application code. When these methods are called from entity listeners the value of auto parameter is true.

The logs contain information about the time of modification, the user who has modified the entity, and the new values of the changed attributes. Log entries are stored in the SEC_ENTITY_LOG table corresponding to the EntityLogItem entity. Changed attribute values are stored in the CHANGES column and are converted to instances of EntityLogAttr entity when they are loaded by the Middleware.

3.9.4.1. Setting Up Entity Log

The simplest way to set up the entity log is using the Administration > Entity Log > Setup application screen.

You can also set up Entity Log by entering some records in the database, if you want to include the configuration to the database initialization scripts.

Logging is configured using the LoggedEntity and LoggedAttribute entities corresponding to SEC_LOGGED_ENTITY and SEC_LOGGED_ATTR tables.

LoggedEntity defines the types of entities that should be logged. LoggedEntity has the following attributes:

  • name (NAME column) – the name of the entity meta-class, for example, sales_Customer.

  • auto (AUTO column) – defines if the system should log the changes when EntityLogAPI is called with auto = true parameter (i.e. called by entity listeners).

  • manual (MANUAL column) – defines if the system should log the changes when EntityLogAPI is called with auto = false parameter.

LoggedAttribute defines the entity attribute to be logged and contains a link to the LoggedEntity and the attribute name.

To set up logging for a certain entity, the corresponding entries should be added into the SEC_LOGGED_ENTITY and SEC_LOGGED_ATTR tables. For example, logging the changes to name and grade attributes of the Customer entity can be enabled using:

insert into SEC_LOGGED_ENTITY (ID, CREATE_TS, CREATED_BY, NAME, AUTO, MANUAL)
values ('25eeb644-e609-11e1-9ada-3860770d7eaf', now(), 'admin', 'sales_Customer', true, true);

insert into SEC_LOGGED_ATTR (ID, CREATE_TS, CREATED_BY, ENTITY_ID, NAME)
values (newid(), now(), 'admin', '25eeb644-e609-11e1-9ada-3860770d7eaf', 'name');

insert into SEC_LOGGED_ATTR (ID, CREATE_TS, CREATED_BY, ENTITY_ID, NAME)
values (newid(), now(), 'admin', '25eeb644-e609-11e1-9ada-3860770d7eaf', 'grade');

The logging mechanism is activated by default. If you want to stop it, set the Enabled attribute of the app-core.cuba:type=EntityLog JMX bean false and then invoke the its invalidateCache() operation. Alternatively, set the cuba.entityLog.enabled application property to false and restart the server.

3.9.4.2. Viewing the Entity Log

The entity log content can be viewed on a dedicated screen available at Administration > Entity Log.

The change log for a certain entity can also be accessed from any application screen by loading a collection of EntityLogItem and the associated EntityLogAttr instances into the data containers and creating the visual components connected to these containers.

The example below shows the fragment of the screen XML descriptor of a Customer entity which has a tab with the entity log content.

The fragment of the customer-edit.xml
<data>
    <instance id="customerDc"
              class="com.company.sample.entity.Customer"
              view="customer-view">
        <loader id="customerDl"/>
    </instance>
    <collection id="entitylogsDc"
                class="com.haulmont.cuba.security.entity.EntityLogItem"
                view="logView" >
        <loader id="entityLogItemsDl">
            <query><![CDATA[select i from sec$EntityLog i where i.entityRef.entityId = :customer
                            order by i.eventTs]]>
            </query>
        </loader>
        <collection id="logAttrDc"
                    property="attributes"/>
    </collection>
</data>
<layout>
    <tabSheet id="tabSheet">
        <tab id="propertyTab">
            <!--...-->
        </tab>
        <tab id="logTab">
            <table id="logTable"
                   dataContainer="entitylogsDc"
                   width="100%"
                   height="100%">
                <columns>
                    <column id="eventTs"/>
                    <column id="user.login"/>
                    <column id="type"/>
                </columns>
            </table>
            <table id="attrTable"
                   height="100%"
                   width="100%"
                   dataContainer="logAttrDc">
                <columns>
                    <column id="name"/>
                    <column id="oldValue"/>
                    <column id="value"/>
                </columns>
            </table>
        </tab>
    </tabSheet>
</layout>

Let us have a look on the Customer screen controller:

The fragment of the Customer screen controller
@UiController("sample_Customer.edit")
@UiDescriptor("customer-edit.xml")
@EditedEntityContainer("customerDc")
public class CustomerEdit extends StandardEditor<Customer> {
    @Inject
    private InstanceLoader<Customer> customerDl;
    @Inject
    private CollectionLoader<EntityLogItem> entityLogItemsDl;

    @Subscribe
    private void onBeforeShow(BeforeShowEvent event) { (1)
        customerDl.load();
    }

    @Subscribe(id = "customerDc", target = Target.DATA_CONTAINER)
    private void onCustomerDcItemChange(InstanceContainer.ItemChangeEvent<Customer> event) { (2)
        entityLogItemsDl.setParameter("customer", event.getItem().getId());
        entityLogItemsDl.load();
    }
}

Notice that the screen has no @LoadDataBeforeShow annotation, because loading is triggered explicitly.

1 − the onBeforeShow method loads data before showing the screen.
2 − in the ItemChangeEvent handler of the customerDc container, a parameter is set to the dependent loader and it is triggered.

Logged attributes should contain the @LocalizedValue annotation in order to display localized values. When annotated, the logging mechanism populates the EntityLogAttr.messagesPack field, and the table in the example above is able to use locValue column instead of value:

<table id="attrTable" width="100%" height="200px" dataContainer="logAttrDc">
  <columns>
      <column id="name"/>
      <column id="locValue"/>
  </columns>
</table>

3.9.5. Entity Snapshots

The entity saving mechanism, much like the entity log, is intended to track data changes at runtime. It has the following distinct features:

  • The whole state (or snapshot) of a graph of entities defined by a specified view is saved.

  • Snapshot saving mechanism is explicitly called from the application code.

  • The platform allows the snapshots to be viewed and compared.

3.9.5.1. Saving Snapshots

In order to save a snapshot of a given graph of entities, you need to call the EntitySnapshotService.createSnapshot() method passing the entity which is an entry point to the graph and the view describing the graph. The snapshot will be created using the loaded entities without any calls to the database. As a result, the snapshot will not contain the fields that are not included in the view used to load the entity.

The graph of Java objects is converted into XML and saved in the SYS_ENTITY_SNAPSHOT table (corresponding to the EntitySnapshot enitity) together with the link to the primary entity.

Usually, snapshots need to be saved after editor screen commit. This may be achieved by overriding the postCommit() method of the screen controller, for example:

public class CustomerEditor extends AbstractEditor<Customer> {

    @Inject
    protected Datasource<Customer> customerDs;
    @Inject
    protected EntitySnapshotService entitySnapshotService;

...
    @Override
    protected boolean postCommit(boolean committed, boolean close) {
        if (committed) {
            entitySnapshotService.createSnapshot(customerDs.getItem(), customerDs.getView());
        }
        return super.postCommit(committed, close);
    }
}
3.9.5.2. Viewing Snapshots

Viewing snapshots for arbitrary entities is possible using the com/haulmont/cuba/gui/app/core/entitydiff/diff-view.xml frame. For example:

<frame id="diffFrame"
      src="/com/haulmont/cuba/gui/app/core/entitydiff/diff-view.xml"
      width="100%"
      height="100%"/>

The snapshots should be loaded into the frame from the edit screen controller:

public class CustomerEditor extends AbstractEditor<Customer> {

    @Inject
    private EntityStates entityStates;
    @Inject
    protected EntityDiffViewer diffFrame;

...
    @Override
    protected void postInit() {
        if (!entityStates.isNew(getItem())) {
            diffFrame.loadVersions(getItem());
        }
    }
}

The diff-view.xml frame shows the list of snapshots for the given entity, with an ability to compare them. The view for each snapshot includes the user, date and time. When a snapshot is selected from the list, the changes will be displayed compared to the previous snapshot. All attributes are marked as changed for the first snapshot. Selecting two snapshots shows the results of the comparison in a table.

The comparison table shows attribute names and their new values. When a row is selected, the detailed information on attribute changes across two snapshots is shown. Reference fields are displayed according to their instance name. When comparing collections, the new and removed elements are highlighted with green and red color respectively. Collection elements with changed attributes are displayed without highlighting. Changes to element positions are not recorded.

3.9.6. Entity Statistics

The entity statistics mechanism provides the information on the current number of entity instances in the database. This data is used to automatically select the best lookup strategy for linked entities and to limit the size of search results displayed in UI screens.

Statistics is stored in the SYS_ENTITY_STATISTICS table which is mapped to the EntityStatistics entity. It can be updated automatically using the refreshStatistics() method of the PersistenceManagerMBean JMX bean. If you pass an entity name as a parameter, the statistics will be collected for the given entity, otherwise - for all entities. If you want to update the statistics regularly, create a scheduled task invoking this method. Keep it mind that the collection process will execute select count(*) for each entity and can put significant load on the database.

Programmatic access to entity statistics is available via PersistenceManagerAPI interface on the middle tier and PersistenceManagerService on the the client tier. Statistics is cached in memory, so any direct changes to statistics in the database will be applied only after the server restart or after calling the PersistenceManagerMBean.flushStatisticsCache() method.

The EntityStatistics attributes are described below.

  • name (NAME column) – the name of the entity meta-class, for example, sales_Customer.

  • instanceCount (INSTANCE_COUNT column) – the approximate number of entity instances.

  • fetchUI (FETCH_UI column) – the size of the data displayed on a page when extracting entity lists.

    For example, the Filter component uses this number in the Show N rows field.

  • maxFetchUI (MAX_FETCH_UI column) – the maximum number of entity instances that can be extracted and passed to the client tier.

    This limit is applied when showing entity lists in such components as LookupField or LookupPickerField, as well as tables without a filter, when no limitations are applied to the connected data loader via CollectionLoader.setMaxResults(). In this case the data loader itself limits the number of extracted instances to maxFetchUI.

  • lookupScreenThreshold (LOOKUP_SCREEN_THRESHOLD column) – the threshold, measured in number of entities, which determines when lookup screens should be used instead of dropdowns for entity lookup.

    The Filter component takes this parameter into account when choosing filter parameters. Until the threshold is reached, the system uses the LookupField component, and once the threshold is exceeded, the PickerField component is used. Hence, if lookup screens should be used for a specific entity in a filter parameter, it is possible to set the value of lookupScreenThreshold to a value lower than instanceCount.

PersistenceManagerMBean JMX bean enables setting default values for all of the parameters mentioned above via DefaultFetchUI, DefaultMaxFetchUI, DefaultLookupScreenThreshold attributes. The system will use the corresponding default values when an entity has no statistics, which is a common case.

Besides, PersistenceManagerMBean.enterStatistics() method allows a user to enter statistics data for an entity. For example, the following parameters should be passed to the method to set a default page size to 1,000 and maximum number of loaded into LookupField instances to 30,000:

entityName: sales_Customer
fetchUI: 1000
maxFetchUI: 30000

Another example: suppose that you have a filter condition by the Customer entity, and you want to use a lookup screen instead of dropdown list when selecting Customer in the condition parameter. Then invoke the enterStatistics() method with the following parameters:

entityName: sales_Customer
instanceCount: 2
lookupScreenThreshold: 1

Here we ignore the actual number of Customer records in the database and manually specify that the threshold is always exceeded.

3.9.7. Export and Import Entities in JSON

The platform provides an API for exporting and importing graphs of entities in JSON format. It is available on Middleware via the EntityImportExportAPI interface and on the client tier via EntityImportExportService. These interfaces have an identical set of methods which are described below. The export/import implementation delegates to the EntitySerializationAPI interface which can also be used directly.

  • exportEntitiesToJSON() - serializes a collection of entities to JSON.

    @Inject
    private EntityImportExportService entityImportExportService;
    @Inject
    private GroupDatasource<Customer, UUID> customersDs;
    
    ...
    String jsonFromDs = entityImportExportService.exportEntitiesToJSON(customersDs.getItems());
  • exportEntitiesToZIP() - serializes a collection of entities to JSON and packs the JSON file into ZIP archive. In the following example, the ZIP archive is saved to the file storage using the FileLoader interface:

    @Inject
    private EntityImportExportService entityImportExportService;
    @Inject
    private GroupDatasource<Customer, UUID> customersDs;
    @Inject
    private Metadata metadata;
    @Inject
    private DataManager dataManager;
    
    ...
    byte[] array = entityImportExportService.exportEntitiesToZIP(customersDs.getItems());
    FileDescriptor descriptor = metadata.create(FileDescriptor.class);
    descriptor.setName("customersDs.zip");
    descriptor.setExtension("zip");
    descriptor.setSize((long) array.length);
    descriptor.setCreateDate(new Date());
    try {
        fileLoader.saveStream(descriptor, () -> new ByteArrayInputStream(array));
    } catch (FileStorageException e) {
        throw new RuntimeException(e);
    }
    dataManager.commit(descriptor);
  • importEntitiesFromJSON() - deserializes the JSON and persists deserialized entities according to the rules, described by the entityImportView parameter (see JavaDocs on the EntityImportView class). If an entity is not present in the database, it will be created. Otherwise the fields of the existing entity that are specified in the entityImportView will be updated.

  • importEntitiesFromZIP() - reads a ZIP archive that contains a JSON file, deserializes the JSON and persists deserialized entities like the importEntitiesFromJSON() method.

    @Inject
    private EntityImportExportService entityImportExportService;
    @Inject
    private FileLoader fileLoader;
    
    private FileDescriptor descriptor;
    
    ...
    EntityImportView view = new EntityImportView(Customer.class);
    view.addLocalProperties();
    try {
        byte[] array = IOUtils.toByteArray(fileLoader.openStream(descriptor));
        Collection<Entity> collection = entityImportExportService.importEntitiesFromZIP(array, view);
    } catch (FileStorageException e) {
        throw new RuntimeException(e);
    }

3.9.8. File Storage

File storage enables uploading, storing and downloading arbitrary files associated with the entities. In the standard implementation, the files are stored outside of the main database using a specialized structure within the file system.

File storage mechanism includes the following parts:

  • FileDescriptor entity – the descriptor of the uploaded file (not to be confused with java.io.FileDescriptor) enables referencing the file from the data model objects.

  • FileStorageAPI interface – provides access to the file storage at the middle tier. Its main methods are:

    • saveStream() – saves the contents of the file passed as the InputStream according to the specified FileDescriptor.

    • openStream() – returns the contents of the file defined by the FileDescriptor in the form of an opened InputStream.

  • FileUploadController class – a Spring MVC controller, which enables sending files from the Client to the Middleware with HTTP POST requests.

  • FileDownloadController class – Spring MVC controller which enables retrieving files from the Middleware to the Client with HTTP GET requests.

  • FileUpload and FileMultiUpload visual components – enable uploading files from the user’s computer to the client tier of the application and then transferring them to the Middleware.

  • FileUploadingAPI interface – temporary storage for files uploaded to the client tier. It is used for uploading files to the client tier by the visual components mentioned above. The application code can use putFileIntoStorage() method for moving a file into the persistent storage of the Middleware.

  • FileLoader - an interface for working with the file storage using the same set of methods on both middle and client tiers.

  • ExportDisplay – client tier interface allowing downloading various application resources to the user’s computer. Files can be retrieved from persistent storage using the show() method, which requires a FileDescriptor. An instance of ExportDisplay may be obtained either by calling the AppConfig.createExportDisplay() static method, or through injection into the controller class.

File transfer between the user’s computer and the storage in both directions is always performed by copying data between input and output streams. Files are never fully loaded into memory at any application level, which enables transferring files of almost any size.

3.9.8.1. Uploading Files

Files from the user’s computer can be uploaded into the storage using the FileUpload and FileMultiUpload components. Usage examples are provided in this manual in the appropriate component descriptions, as well as in Loading and Displaying Images.

FileUpload component is also available within the ready-to-use FileUploadDialog window designed to load files in the temporary storage.

The temporary client-level storage (FileUploadingAPI) stores temporary files in the folder defined by cuba.tempDir application property. Temporary files can remain in the folder in case of any failures. The clearTempDirectory() method of the cuba_FileUploading bean is invoked periodically by the scheduler defined in the cuba-web-spring.xml file.

The FileUploadDialog window provides the base functionality for loading files into the temporary storage. It contains the drop zone for drag-and-dropping files from outside of the browser and the upload button.

gui fileUploadDialog

The dialog is implemented with the legacy screen API and can be used as follows:

@Inject
private Screens screens;

@Inject
private FileUploadingAPI fileUploadingAPI;

@Inject
private DataManager dataManager;

@Subscribe("showUploadDialogBtn")
protected void onShowUploadDialogBtnClick(Button.ClickEvent event) {
    FileUploadDialog dialog = (FileUploadDialog) screens.create("fileUploadDialog", OpenMode.DIALOG);
    dialog.addCloseWithCommitListener(() -> {
        UUID fileId = dialog.getFileId();
        String fileName = dialog.getFileName();

        File file = fileUploadingAPI.getFile(fileId); (1)

        FileDescriptor fileDescriptor = fileUploadingAPI.getFileDescriptor(fileId, fileName); (2)
        try {
            fileUploadingAPI.putFileIntoStorage(fileId, fileDescriptor); (3)
            dataManager.commit(fileDescriptor); (4)
        } catch (FileStorageException e) {
            throw new RuntimeException(e);
        }
    });
    screens.show(dialog);
}
1 - obtaining the java.io.File object which points to the file in the file system of the Web Client block. You may need it if you want to process the file somehow instead of just putting it into the file storage.
2 - creating a FileDescriptor entity.
3 - uploading file to the file storage of the middle tier.
4 - saving the FileDescriptor entity.

After successful upload, the dialog is closed with COMMIT_ACTION_ID. In a CloseWithCommitListener, use the getFileId() and getFileName() methods to get the UUID and the name of uploaded file. Then you can get the file itself or create a FileDescriptor and upload the file to the file storage, or implement any other logic.

3.9.8.2. Downloading Files

Files can be downloaded from the file storage to the user’s computer by using the ExportDisplay interface. It can be obtained by calling the AppConfig.createExportDisplay() static method or via injection in the controller class. For example:

AppConfig.createExportDisplay(this).show(fileDescriptor);

The show() method accepts an optional ExportFormat type parameter, which defines the type of the content and the file extension. If the format has not been provided, the extension is retrieved from the FileDescriptor, and the content type is set to application/octet-stream.

The file extension defines whether the file is downloaded via the browser’s standard open/save dialog (Content-Disposition = attachment), or if the browser will attempt to show the file in the browser window (Content-Disposition = inline). The list of extensions for files that should be shown in the browser window is defined by the cuba.web.viewFileExtensions application property.

ExportDisplay also enables downloading of arbitrary data if ByteArrayDataProvider is used as a parameter of the show() method. For example:

public class SampleScreen extends AbstractWindow {

    @Inject
    private ExportDisplay exportDisplay;

    public void onDownloadBtnClick(Component source) {
        String html = "<html><head title='Test'></head><body><h1>Test</h1></body></html>";
        byte[] bytes;
        try {
            bytes = html.getBytes("UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
        exportDisplay.show(new ByteArrayDataProvider(bytes), "test.html", ExportFormat.HTML);
    }
}
3.9.8.3. FileLoader Interface

The FileLoader interface allows you to work with file storage using the same set of methods on both middle and client tiers. Uploading and downloading of files is performed using streams:

  • saveStream() – saves an InputStream contents into file storage.

  • openStream() – returns an input stream to load a file contents from file storage.

Both client-side and server-side implementations of FileLoader follow the common rule: file transfer is always performed by copying data between input and output streams. Files are never fully loaded into memory at any application level, which enables transferring files of almost any size.

As an example of using FileLoader let’s consider a simple task of saving a user input into the text file and displaying the file content in another field on the same screen.

The screen contains two textArea fields. Suppose the user inputs text in the first textArea, clicks the buttonIn below, and the text is saved to the FileStorage. The second textArea will display the content of the saved file on buttonOut click.

Below is the fragment of the screen XML descriptor:

<hbox margin="true"
      spacing="true">
    <vbox spacing="true">
        <textArea id="textAreaIn"/>
        <button id="buttonIn"
                caption="Save text in file"
                invoke="onButtonInClick"/>
    </vbox>
    <vbox spacing="true">
        <textArea id="textAreaOut"
                  editable="false"/>
        <button id="buttonOut"
                caption="Show the saved text"
                invoke="onButtonOutClick"/>
    </vbox>
</hbox>

The screen controller contains two methods invoked on buttons click:

  • In the onButtonInClick() method we create a byte array from the first textArea input. Then we create a FileDescriptor object and define the new file name, extension, size, and creation date with its attributes.

    Then we save the new file with the saveStream() method of FileLoader, passing the FileDescriptor to it and providing the file content with an InputStream supplier. We also commit the FileDescriptor to the data store using the DataManager interface.

  • In the onButtonOutClick() method we extract the content of the saved file using the openStream() method of the FileLoader. Then we display the content of the file in the second textArea.

import com.haulmont.cuba.core.entity.FileDescriptor;
import com.haulmont.cuba.core.global.DataManager;
import com.haulmont.cuba.core.global.FileLoader;
import com.haulmont.cuba.core.global.FileStorageException;
import com.haulmont.cuba.core.global.Metadata;
import com.haulmont.cuba.gui.components.AbstractWindow;
import com.haulmont.cuba.gui.components.ResizableTextArea;
import com.haulmont.cuba.gui.upload.FileUploadingAPI;
import org.apache.commons.io.IOUtils;

import javax.inject.Inject;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;

public class FileLoaderScreen extends AbstractWindow {

    @Inject
    private Metadata metadata;
    @Inject
    private FileLoader fileLoader;
    @Inject
    private DataManager dataManager;
    @Inject
    private ResizableTextArea textAreaIn;
    @Inject
    private ResizableTextArea textAreaOut;

    private FileDescriptor fileDescriptor;

    public void onButtonInClick() {
        byte[] bytes = textAreaIn.getRawValue().getBytes();

        fileDescriptor = metadata.create(FileDescriptor.class);
        fileDescriptor.setName("Input.txt");
        fileDescriptor.setExtension("txt");
        fileDescriptor.setSize((long) bytes.length);
        fileDescriptor.setCreateDate(new Date());

        try {
            fileLoader.saveStream(fileDescriptor, () -> new ByteArrayInputStream(bytes));
        } catch (FileStorageException e) {
            throw new RuntimeException(e);
        }

        dataManager.commit(fileDescriptor);
    }

    public void onButtonOutClick() {
        try {
            InputStream inputStream = fileLoader.openStream(fileDescriptor);
            textAreaOut.setValue(IOUtils.toString(inputStream));
        } catch (FileStorageException | IOException e) {
            throw new RuntimeException(e);
        }
    }
}
fileLoader recipe
3.9.8.4. Standard File Storage Implementation

The standard implementation stores files in a dedicated folder structure within one or several file locations.

The roots of the structure can be defined in the cuba.fileStorageDir application property in the format of comma-separated paths list. For example:

cuba.fileStorageDir=/work/sales/filestorage,/mnt/backup/filestorage

If the property is not defined, the storage will be located in the filestorage sub-folder of the Middleware’s work directory. This folder is tomcat/work/app-core/filestorage in standard Tomcat deployment.

With several locations defined, the storage behaves as follows:

  • First folder in the list is considered as primary, others – as backup.

  • Stored files are first placed in the primary folder, and then copied to all of the backup directories.

    The system checks that each folder is accessible before storing a file. If the primary directory is not accessible, the system throws an exception without storing the file. If any of the backup directories are not accessible, the file gets stored in available ones and the corresponding error is logged.

  • The files are read from the primary directory.

    If the primary directory is not accessible, the system reads files from the first available backup directory containing the required files. A corresponding error is logged.

The storage folder structure is organized in the following way:

  • There are three levels of subdirectories representing the files upload date – year, month, and day.

  • The actual files are saved in the day directory. The file names match the identifiers of the corresponding FileDescriptor objects. The file extension matches that of the source file.

  • The root folder of the structure contains a storage.log file with the information on each stored file, including the user and upload time. This log is not required for operation of the storage mechanism, but it could be useful for troubleshooting.

The app-core.cuba:type=FileStorage JMX bean displays the current set of storage roots and offers the following methods for troubleshooting:

  • findOrphanDescriptors() – finds all instances of FileDescriptor in the database that do not have a matching file in the storage.

  • findOrphanFiles() – finds all files in the storage that do not have a corresponding FileDescriptor instance in the database.

3.9.8.5. Amazon S3 File Storage Implementation

The standard file storage implementation can be replaced by a cloud storage service. We recommend to use separate cloud file storage services for cloud deployments which, commonly, don’t guarantee the persistence of external files on their hard drives.

The platform provides support of Amazon S3 file storage service out-of-the-box. To support other services, you need to implement your custom logic.

To add Amazon S3 support, firstly, you need to register AmazonS3FileStorage class in the spring.xml file of the core module:

<bean name="cuba_FileStorage"
          class="com.haulmont.cuba.core.app.filestorage.amazon.AmazonS3FileStorage"/>

Next, you should define your Amazon settings in the app.properties file in the core module:

cuba.amazonS3.accessKey = <Access Key>
cuba.amazonS3.secretAccessKey = <Secret Access Key>
cuba.amazonS3.region = <Region>
cuba.amazonS3.bucket = <Bucket Name>

The accessKey and secretAccessKey should be those of your AWS IAM user account, not the AWS account itself. You can find the correct credentials on the Users tab of your AWS console.

The storage folder structure is organized similarly to the standard implementation.

3.9.9. Folders Panel

The folders panel provides quick access to frequently used information. It is a panel on the left side of the main application window containing a hierarchical structure of folders. Clicking on folders shows the corresponding system screens with certain parameters.

At the moment of writing, the panel is available for the Web Client only.

The platform supports three types of folders: application folders, search folders and record sets. Application folders are displayed at the top of the panel as a separate folder tree. Search folders and record sets are displayed at the bottom of the panel in a combined tree. Folders support keyboard shortcuts if the cuba.web.foldersPaneEnabled property is true.

  • Application folders:

    • Open screens with or without a filter.

    • The set of folders depend on the current user session. Folder visibility is defined by a Groovy script.

    • Application folders can be created and changed only by users with special permissions.

    • Folder headers may show the record count calculated by a Groovy script.

    • Folder headers are updated on timer events, which means that record count and display style for each folder can be updated.

  • Search folders:

    • Open screens with a filter.

    • Search folders can be local or global, accessible only by the user who created them or by all users, respectively.

    • Local folders can be created by any user, while global are created only by users with special permissions.

  • Record sets:

    • Open screens with a filter containing a condition to select specific records by their identifiers.

    • Record set content can be edited using the dedicated table actions: Add to set and Remove from set.

    • Record sets are available only to the user who created them.

The following application properties can influence the functionality of the folder panel:

3.9.9.1. Application Folders

Creating application folders requires special permissions to create/edit application folders (cuba.gui.appFolder.global).

A simple application folder can be created via the folder panel context menu. Such folder will not be connected to the system screens and can be only used to group other folders within a folder tree.

A folder that opens a screen with a filter can be created as follows:

  • Open a screen and filter the records as necessary.

  • Select Save as application folder option in the Filter…​ button menu.

  • Fill in the folder attributes in the Add dialog:

    • Folder name.

    • Screen Caption – a string to be added to the window title when opening it from the folder.

    • Parent folder – determines the location of the new folder in the folder tree.

    • Visibility script – a Groovy script defining folder visibility, executed at the start of user session.

      The script should return a Boolean. The folder is visible, if the script is not defined or returns true or null. Example of a Groovy script:

      userSession.currentOrSubstitutedUser.login == 'admin'
    • Quantity script – a Groovy script defining the record count and display style for a folder. Executed at the start of the user session and on timer.

      The script should return a numeric value, the integer part of which will be used as the record count value. If the script is not defined or returns null, the counter will not be displayed. In addition to the returned value, the script can also set the style variable, which will be used as folder display style. Example of a Groovy script:

      def em = persistence.getEntityManager()
      def q = em.createQuery('select count(o) from sales_Order o')
      def count = q.getSingleResult()
      
      style = count > 0 ? 'emphasized' : null
      return count

      In order for the style to be displayed, the application theme should contain this style for the v-tree-node element in cuba-folders-pane, for example:

      .c-folders-pane .v-tree-node.emphasized {
        font-weight: bold;
      }
    • Order No – the folder’s order number in the folder tree.

Scripts can use the following variables defined in the groovy.lang.Binding context:

  • folder – an instance of AppFolder entity for which the script is executed.

  • userSession – instance of UserSession for current user session.

  • persistence – implementation of the Persistence interface.

  • metadata – implementation of the Metadata interface.

The platform uses the same instance of groovy.lang.Binding for all scripts when the folders are being updated. So it is possible to pass variables between them in order to eliminate duplicate requests and increase performance.

Script sources can be stored within the attributes of the AppFolder entity or in separate files. In the latter case, the attribute should include a file path with a mandatory ".groovy" extension, as required by the Resources interface. If an attribute contains a string ending with ".groovy", the script will be loaded from the corresponding file; otherwise, the attribute content itself will be used as a script.

Application folders are instances of the AppFolder entity and are stored in the related SYS_FOLDER and SYS_APP_FOLDER tables.

3.9.9.2. Search Folders

Search folders can be created by the users similar to application folders. Group folders are created directly via the context menu of the folder panel. The folders connected to screens are created from the Filter…​ button menu, using the Save as search folder option.

Creating global search folders, requires the user to have Create/edit global search folders permission (cuba.gui.searchFolder.global).

Search folder’s filter can be edited once the folder is created by opening the folder and changing the Folder:{folder name} filter. Saving the filter will change the folder filter as well.

Search folders are instances of the SearchFolder entity stored in the related SYS_FOLDER and SEC_SEARCH_FOLDER tables.

3.9.9.3. Record Sets

Using records sets within a screen is possible, if the Filter has a corresponding Table component defined in the applyTo attribute. For example:

<layout>
  <filter id="customerFilter"
          datasource="customersDs"
          applyTo="customersTable"/>

  <groupTable id="customersTable"
              width="100%">
      <buttonsPanel>
          <button action="customersTable.create"/>
...
      </buttonsPanel>
...

Add to set or Add to current set / Remove from set buttons should now appear in table context menu. If a table includes a buttonsPanel (as in the example above), the corresponding table buttons will also be added.

Record sets are the instances of the SearchFolder entity stored in the related SYS_FOLDER and SEC_SEARCH_FOLDER tables.

3.9.10. Information about Software Components

The platform provides an ability to register the information about third party software components used in the application (credits) and to display this information in the UI. The information includes a software component name, a website link and the license text.

Application components of the platform contain their own files with descriptions, like com/haulmont/cuba/credits.xml, com/haulmont/reports/credits.xml and so on. The cuba.creditsConfig application property can be used to specify a description file of the application.

The structure of the credits.xml file is as follows:

  • The items element lists the used libraries with license texts included either as an embedded license element, or as a license attribute with a link to the text in the licenses section.

    It is possible to reference licenses declared in the current file as well as any other file declared in cuba.creditsConfig variable prior to the current one.

  • The licenses element lists the texts of general licenses used (e.g. LGPL).

The entire list of third-party software components can be displayed using the com/haulmont/cuba/gui/app/core/credits/credits-frame.xml frame, which loads the information from the files defined in the cuba.creditsConfig property. An example of the frame within a screen:

<dialogMode width="500" height="400"/>
<layout expand="creditsBox">
  <groupBox id="creditsBox"
            caption="msg://credits"
            width="100%">
      <frame id="credits"
              src="/com/haulmont/cuba/gui/app/core/credits/credits-frame.xml"
              width="100%"
              height="100%"/>
  </groupBox>
</layout>

If the dialog mode (WindowManager.OpenType.DIALOG) is used when opening the screen that contains the frame, the height must be specified; otherwise, the scrolling may work not correctly. See the dialogMode element in the example above.

3.9.11. Integration with MyBatis

MyBatis framework offers wider capabilities for running SQL and mapping query results to objects than ORM native query or QueryRunner.

Follow the steps below to set up integration with MyBatis in a CUBA project.

  1. Create UUID type handler class in the root package of the core module.

    import com.haulmont.cuba.core.global.UuidProvider;
    import org.apache.ibatis.type.JdbcType;
    import org.apache.ibatis.type.TypeHandler;
    
    import java.sql.*;
    
    public class UUIDTypeHandler implements TypeHandler {
    
        @Override
        public void setParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException {
            ps.setObject(i, parameter, Types.OTHER);
        }
    
        @Override
        public Object getResult(ResultSet rs, String columnName) throws SQLException {
            String val = rs.getString(columnName);
            if (val != null) {
                return UuidProvider.fromString(val);
            } else {
                return null;
            }
        }
    
        @Override
        public Object getResult(ResultSet rs, int columnIndex) throws SQLException {
            String val = rs.getString(columnIndex);
            if (val != null) {
                return UuidProvider.fromString(val);
            } else {
                return null;
            }
        }
    
        @Override
        public Object getResult(CallableStatement cs, int columnIndex) throws SQLException {
            String val = cs.getString(columnIndex);
            if (val != null) {
                return UuidProvider.fromString(val);
            } else {
                return null;
            }
        }
    }
  2. Create mybatis.xml configuration file in the core module next to spring.xml with a correct reference to UUIDTypeHandler:

    <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
    <configuration>
        <settings>
            <setting name="lazyLoadingEnabled" value="false"/>
        </settings>
        <typeHandlers>
            <typeHandler javaType="java.util.UUID"
                         handler="com.company.demo.core.UUIDTypeHandler"/>
        </typeHandlers>
    </configuration>
  3. Add the following beans into spring.xml file to use MyBatis in the project:

    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="cubaDataSource"/>
        <property name="configLocation" value="com/company/demo/mybatis.xml"/>
        <property name="mapperLocations" value="com/company/demo/core/sqlmap/*.xml"/>
    </bean>
    
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com/company/demo.core.dao"/>
        <property name="sqlSessionFactory" ref="sqlSessionFactory"/>
    </bean>
    
    <bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
        <constructor-arg index="0" ref="sqlSessionFactory" />
    </bean>

    The sqlSessionFactory bean provides the reference to the created mybatis.xml.

    The MapperLocations parameter defines a path to mapperLocations mapping files (according to the rules of ResourceLoader Spring interface).

  4. Finally, add MyBatis dependencies to the core module in build.gradle:

    compile('org.mybatis:mybatis:3.2.8')
    compile('org.mybatis:mybatis-spring:1.2.5')

Below is an example of a mapping file for loading an instance of Order together with a related Customer and a collection of Order items:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.sample.sales">

    <select id="selectOrder" resultMap="orderResultMap">
        select
        o.ID as order_id,
        o.DATE as order_date,
        o.AMOUNT as order_amount,
        c.ID as customer_id,
        c.NAME as customer_name,
        c.EMAIL as customer_email,
        i.ID as item_id,
        i.QUANTITY as item_quantity,
        p.ID as product_id,
        p.NAME as product_name
        from
        SALES_ORDER o
        left join SALES_CUSTOMER c on c.ID = o.CUSTOMER_ID
        left join SALES_ITEM i on i.ORDER_ID = o.id and i.DELETE_TS is null
        left join SALES_PRODUCT p on p.ID = i.PRODUCT_ID
        where
        c.id = #{id}
    </select>

    <resultMap id="orderResultMap" type="com.sample.sales.entity.Order">
        <id property="id" column="order_id"/>
        <result property="date" column="order_date"/>
        <result property="amount" column="order_amount"/>

        <association property="customer" column="customer_id" javaType="com.sample.sales.entity.Customer">
            <id property="id" column="customer_id"/>
            <result property="name" column="customer_name"/>
            <result property="email" column="customer_email"/>
        </association>

        <collection property="items" ofType="com.sample.sales.entity.Item">
            <id property="id" column="item_id"/>
            <result property="quantity" column="item_quantity"/>
            <association property="product" column="product_id" javaType="com.sample.sales.entity.Product">
                <id property="id" column="product_id"/>
                <result property="name" column="product_name"/>
            </association>
        </collection>
    </resultMap>

</mapper>

The following code can be used to retrieve query results from the example above:

try (Transaction tx = persistence.createTransaction()) {
    SqlSession sqlSession = AppBeans.get("sqlSession");
    Order order = (Order) sqlSession.selectOne("com.sample.sales.selectOrder", orderId);
    tx.commit();
}

3.9.12. Pessimistic Locking

Pessimistic locking should be used when there is a high probability of simultaneous editing of a single entity instance. In such cases the standard optimistic locking, based on entity versioning, usually creates too many collisions.

Pessimistic locking explicitly locks an entity instance when it is opened in the editor. As a result, only one user can edit this particular entity instance in a given moment of time.

Pessimistic locking mechanism can also be used to manage simultaneous execution of arbitrary processes. The key benefit is that the locks are distributed, since they are replicated in the Middleware cluster. More information is available in JavaDocs for the LockManagerAPI and LockService interfaces.

Pessimistic locking can be enabled for any entity class on application development or production stage using Administration > Locks > Setup screen, or as follows:

  • Insert a new record with the following field values into the SYS_LOCK_CONFIG table with the following field values:

    • ID – an arbitrary UUID-type identifier.

    • NAME – the name of the object to be locked. For an entity, it should be the name of its meta class.

    • TIMEOUT_SEC – lock expiration timeout in seconds.

    Example:

    insert into sys_lock_config (id, create_ts, name, timeout_sec) values (newid(), current_timestamp, 'sales_Order', 300)
  • Restart the server or call reloadConfiguration() method of the app-core.cuba:type=LockManager JMX bean.

Current state of locks can be tracked via the app-core.cuba:type=LockManager JMX bean or through the Administration > Locks screen. This screen also enables unlocking of any object.

3.9.13. Running SQL Using QueryRunner

QueryRunner is a class designed to run SQL. It should be used instead of JDBC in all cases where using plain SQL is necessary and working with the ORM tools of the same purpose is not desired.

The platform’s QueryRunner is a variant of Apache DbUtils QueryRunner with the added ability to use Java Generics.

Usage example:

QueryRunner runner = new QueryRunner(persistence.getDataSource());
try {
  Set<String> scripts = runner.query("select SCRIPT_NAME from SYS_DB_CHANGELOG",
          new ResultSetHandler<Set<String>>() {
              public Set<String> handle(ResultSet rs) throws SQLException {
                  Set<String> rows = new HashSet<String>();
                  while (rs.next()) {
                      rows.add(rs.getString(1));
                  }
                  return rows;
              }
          });
  return scripts;
} catch (SQLException e) {
  throw new RuntimeException(e);
}

There are two ways of using QueryRunner: current transaction or separate transaction in autocommit mode.

  • To run a query in current transaction QueryRunner must be instantiated using a parameterless constructor. Then, query() or update() methods should be called with a Connection parameter retrieved via EntityManager.getConnection(). There is no need to close the Connection after the query, as it will be closed when the transaction is committed.

  • To run a query in a separate transaction, QueryRunner instance must be created using a constructor with the DataSource parameter retrieved using Persistence.getDataSource(). Then, query() or update() methods should be called without the Connection parameter. Connection will be created from the specified DataSource and immediately closed afterwards.

3.9.14. Scheduled Tasks Execution

The platform offers two ways to run scheduled tasks:

  • By using the standard TaskScheduler mechanism of the Spring Framework.

  • By using platform’s own mechanism of scheduled tasks execution.

3.9.14.1. Spring TaskScheduler

This mechanism is described in details in the Task Execution and Scheduling section of the Spring Framework manual.

TaskScheduler can be used to run methods of arbitrary Spring beans in any application block both at the middleware and client tiers.

Example of configuration in spring.xml:

<?xml version="1.0" encoding="UTF-8"?>
<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"
       xmlns:task="http://www.springframework.org/schema/task"
       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 http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-4.3.xsd">

  <!--...-->

  <task:scheduled-tasks scheduler="scheduler">
      <task:scheduled ref="sales_Processor" method="someMethod" fixed-rate="60000"/>
      <task:scheduled ref="sales_Processor" method="someOtherMethod" cron="0 0 1 * * MON-FRI"/>
  </task:scheduled-tasks>
</beans>

In the example above, two tasks are declared, which invoke someMethod() and someOtherMethod() of the sales_Processor bean. someMethod() will be invoked at fixed time intervals (60 seconds) from the moment of application startup. someOtherMethod() is invoked according to the schedule specified by Cron expression (for the description of the format of such expressions, see https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/scheduling/support/CronSequenceGenerator.html).

The actual launch of tasks is performed by the bean specified in the scheduler attribute of the scheduled-tasks element. It is the bean of the CubaThreadPoolTaskScheduler class which is configured in the core and web modules of the cuba application component (see cuba-spring.xml, cuba-web-spring.xml). This class provides some CUBA-specific housekeeping functionality.

In order to provide SecurityContext to the code executed by Spring scheduled tasks on the middle tier, use System Authentication.

3.9.14.2. CUBA Scheduled Tasks

CUBA scheduled tasks mechanism is intended to perform scheduled execution of arbitrary Spring beans methods on Middleware. The purposes of this mechanism and its distinction from the above mentioned standard Spring Framework schedulers are:

  • The ability to configure tasks while running an application without restarting the server.

  • The coordination of singleton tasks in the Middleware cluster, including:

    • Reliable protection from simultaneous execution.

    • Binding tasks to servers by priorities.

A singleton task is a task which must be executed only on one server at a certain moment of time. For example, reading from a queue and sending emails.

3.9.14.2.1. Task Registration

Tasks are registered in the SYS_SCHEDULED_TASK database table, which corresponds to the ScheduledTask entity. There are browser and editor screens for working with tasks, which are available through the AdministrationScheduled Tasks menu.

Task attributes are described below:

  • Defined by – describes which software object implements the task. Possible values are:

    • Bean – the task is implemented by a method of a Spring managed bean. Additional attributes:

      • Bean name – the name of the managed bean.

        The bean is listed and available for selection only if it is defined in the core module and has an interface, which contains methods appropriate for invocation from the task. Beans without an interface are not supported.

      • Method name – the bean interface method that is executed. The method must either have no parameters, or all parameters must be of String type. The return type of the method can either be void or String. In the latter case the returning value will be stored in the executions table (see Log finish below).

      • Method parameters – the parameters of the chosen method. Only String type parameters are supported.

    • Class – the task is a class that implements the java.util.concurrent.Callable interface. The class must have a public constructor without parameters. Additional attributes:

      • Class name – the name of the class.

    • Script – the task is a Groovy script. The script is executed by Scripting.runGroovyScript(). Additional attributes:

      • Script name – the name of the script.

  • User name – the name of a user on whose behalf the task will be executed. If not specified, the task will be executed on behalf of the user specified in the cuba.jmxUserLogin application property.

  • Singleton – indicates that the task is a singleton, i.e. should be run only on one application server.

  • Scheduling type – the means of task scheduling:

    • Cron – Cron expression is a sequence of six fields, separated by spaces: second, minute, hour, day, month, day of a week. The month and the day of a week can be represented by the first three letters of their English names. Examples:

      • 0 0 * * * * – the beginning of every hour of every day

      • */10 * * * * * – every 10 seconds

      • 0 0 8-10 * * * – every day at 8, 9 and 10 o’clock

      • 0 0/30 8-10 * * * – every day at 8:00, 8:30, 9:00, 9:30 and 10 o’clock

      • 0 0 9-17 * * MON-FRI – every hour from 9 to 17 on working days

      • 0 0 0 25 DEC ? – every Christmas at midnight.

    • Period – task execution interval in seconds.

    • Fixed Delay – task will be executed with the specified in Period delay after completion of the preceding execution.

  • Period – task execution interval or delay in seconds if Scheduling type is Period or Fixed Delay.

  • Start date – the date/time of the first launch. If not specified, the task is launched immediately on server startup. If specified, the task is launched at startDate + period * N, where N is an integer.

    It is reasonable to specify Start date only for "infrequent" tasks, i.e. running once an hour, once a day, etc.

  • Timeout – time in seconds, upon the expiration of which it is considered that the execution of the task is completed, regardless of whether there is information about task completion or not. If the timeout is not set explicitly, it is assumed to be 3 hours.

    It is recommended to always set the timeout to a realistic value for singleton tasks working in a clustered deployment. With the standard value, if a cluster node executing a task goes down, other nodes will wait for 3 hours before starting a new execution.

  • Time frame – if Start date is specified, Time frame defines the time window in seconds, during which the task will be launched after startDate + period * N time expires. If Time frame is not specified explicitly, it is equal to period / 2.

    If Start date is not specified, Time frame is ignored, i.e. the task will be launched at any time after Period since the previous execution of the task expires.

  • Start delay - delay of execution in seconds after the server is started and scheduling is activated. Set this parameter for a heavy task if you think that it slows down the server startup process.

  • Permitted servers – the list of comma-separated identifiers of servers that have the permission to run this task. If the list is not specified, the task may be executed on any server.

    For singleton tasks, the order of the servers in the list defines the execution priority: the first server has a higher priority than the last. The server with a higher priority will intercept the execution of the singleton as follows: if the server with a higher priority detects that the task has been previously executed by a server with lower priority, it launches the task regardless of whether the Period has elapsed or not.

    Server priority works only if Scheduling type is Period and the Start date attribute is not specified. Otherwise, start occurs at the same time and the interception is impossible.

  • Log start – flags if the task launch should be registered in the SYS_SCHEDULED_EXECUTION table, which corresponds to the ScheduledExecution entity.

    In the current implementation, if the task is a singleton, the launch is registered regardless of this flag.

  • Log finish – flags if the task completion should be registered in the SYS_SCHEDULED_EXECUTION table, which corresponds to the ScheduledExecution entity.

    In the current implementation, if the task is a singleton, completion is registered regardless of this flag.

  • Description – an arbitrary text description of the task.

The task also has activity flag, which can be set in the tasks list screen. Inactive tasks are ignored.

3.9.14.2.2. Tasks Handling Control
  • cuba.schedulingActive application property should be set to true to enable tasks processing. You can do it either in the Administration > Application Properties screen, or through the app-core.cuba:type=Scheduling JMX bean (see its Active attribute).

  • All changes to tasks made via system screens take effect immediately for all servers in the cluster.

  • The removeExecutionHistory() method of the app-core.cuba:type=Scheduling JMX bean can be used to remove old execution history. The method has two parameters:

    • age – the time (in hours) elapsed after the task execution.

    • maxPeriod – the maximum Period (in hours) for tasks that should have their execution history removed. This enables removing the history for frequently run tasks only, while keeping the history for tasks executed once a day.

      The method can be invoked automatically. Create a new task with the following parameters:

      • Bean namecuba_SchedulingMBean

      • Method nameremoveExecutionHistory(String age, String maxPeriod)

      • Method parameters – for example, age = 72, maxPeriod = 12.

3.9.14.2.3. Scheduling Implementation Details
  • Tasks processing invocation (the SchedulingAPI.processScheduledTasks() method) interval is specified in cuba-spring.xml and is equal to 1 second by default. It sets the minimal interval between task launches, which should be twice higher, i.e. 2 seconds. Reducing these values is not recommended.

  • The current implementation of the scheduler is based on the synchronization using row locks in the database table. This means that under significant load the database may not respond to the scheduler in time and it might be necessary to increase the launch interval (>1 second), thus the minimum period of launching tasks will be increased accordingly.

  • If the Permitted servers attribute is not specified, singleton tasks are performed only on the master node of the cluster (in case other conditions are met). It should be kept in mind that a standalone server outside the cluster is also considered a master.

  • The task will not be launched if its previous execution has not yet finished and the specified Timeout has not expired. For singleton tasks in the current implementation, this is achieved using the information in the database; for non-singletons, the execution status table is maintained in the server memory.

  • The execution mechanism creates and caches user sessions for users, specified in the User name attribute of the tasks or in the cuba.jmxUserLogin application property. The session is available in the execution thread of a launched task through the standard UserSessionSource interface.

Precise time synchronization of Middleware servers is required for correct execution of singleton tasks!

See URL History and Navigation section which describes more advanced feature of mapping URL to application screens.

The Web Client block enables opening application screens by commands provided in the URL. If the browser does not have an active session with a logged in user, the application will show the login screen first, and then, after successful authentication, proceed to the main application window with the requested screen.

The list of supported commands is defined by the cuba.web.linkHandlerActions application property. By default, these are open and o. When the HTTP request is being processed, the last part of the URL is analyzed, and if it matches a registered command, control is passed to an appropriate processor, which is a bean implementing the LinkHandlerProcessor interface.

The platform provides a processor that accepts the following request parameters:

  • screen – name of the screen defined in screens.xml, for example:

    http://localhost:8080/app/open?screen=sec$User.browse
  • item – an entity instance to be passed to the edit screen, encoded according to conventions of the EntityLoadInfo class, i.e. entityName-instanceId or entityName-instanceId-viewName. Examples:

    http://localhost:8080/app/open?screen=sec$User.edit&item=sec$User-60885987-1b61-4247-94c7-dff348347f93
    
    http://localhost:8080/app/open?screen=sec$User.edit&item=sec$User-60885987-1b61-4247-94c7-dff348347f93-user.edit

    In order to create a new entity instance directly in the opened editor screen, add the NEW- prefix before the entity class name, for example:

    http://localhost:8080/app/open?screen=sec$User.edit&item=NEW-sec$User
  • params – parameters passed to the screen controller’s init() method. Parameters are encoded as name1:value1,name2:value2. Parameter values may include entity instances encoded according to the conventions of the EntityLoadInfo class. Examples:

    http://localhost:8080/app/open?screen=sales$Customer.lookup&params=p1:v1,p2:v2
    
    http://localhost:8080/app/open?screen=sales$Customer.lookup&params=p1:sales$Customer-01e37691-1a9b-11de-b900-da881aea47a6

If you want to provide additional URL commands, do the following:

  • Create a bean implementing the LinkHandlerProcessor interface in the web module of your project.

  • The canHandle() method of your bean must return true if the current URL, which parameters are passed in the ExternalLinkContext object, should be processed by your bean.

  • In the handle() method, perform required actions.

Your bean can optionally implement Spring’s Ordered interface or contain the Order annotation. Then you can specify the order of your bean in the chain of processors. Use the HIGHEST_PLATFORM_PRECEDENCE and LOWEST_PLATFORM_PRECEDENCE constants of the LinkHandlerProcessor interface to put your bean before or after processors defined in the platform. So if you specify the order lesser than HIGHEST_PLATFORM_PRECEDENCE, your bean will be requested earlier and you can override actions defined by a platform processor if needed.

3.9.16. Sequence Generation

This mechanism enables generating unique numerical sequences via a single API, independent of the DBMS type.

The main part of this mechanism is the UniqueNumbers bean with the UniqueNumbersAPI interface. The bean is available in the Middleware block. The interface has the following methods:

  • getNextNumber() – get the next value in a sequence. The mechanism enables simultaneous management of several sequences, identified by arbitrary strings. The name of the sequence from which you want to retrieve the value is passed in the domain parameter.

    Sequences do not require initialization. When getNextNumber() is called for the first time, the corresponding sequence will be created and 1 will be returned.

  • getCurrentNumber() – obtain the current, i.e. the last generated value of the sequence. The domain parameter sets the sequence name.

  • setCurrentNumber() – set the current value of the sequence. This value incremented by 1 will be returned by the next call to getNextNumber().

Below is an example of getting the next value in a sequence in a Middleware bean:

@Inject
private UniqueNumbersAPI uniqueNumbers;

private long getNextValue() {
  return uniqueNumbers.getNextNumber("mySequence");
}

The getNextNumber() method of the UniqueNumbersService service is used to get sequence values in client blocks.

The app-core.cuba:type=UniqueNumbers JMX bean with methods duplicating the methods of the UniqueNumbersAPI is used for sequence management.

The implementation of the sequence generation mechanism depends on the DBMS type. Sequence parameters can also be managed directly in the database, but in different ways.

  • For HSQL, Microsoft SQL Server 2012+, PostgreSQL and Oracle each UniqueNumbersAPI sequence corresponds to a SEC_UN_{domain} sequence in the database.

  • For Microsoft SQL Server before 2012 each sequence corresponds to a SEC_UN_{domain} table with a primary key of IDENTITY type.

  • For MySQL sequences correspond to records in the SYS_SEQUENCE table.

3.9.17. User Session Log

This mechanism is designed for retrieving historical data on the users' login and logout by the system administrators. The logging mechanism is based on tracking user sessions. Each time the UserSession object is created, the log record is saved to the database containing the following fields:

  • user session ID.

  • user ID.

  • substituted user ID.

  • user’s last action (login / logout / expiration / termination).

  • remote IP address where login request came from.

  • user session client type (web, portal).

  • server ID (for example, localhost:8080/app-core).

  • event start date.

  • event end date.

  • client information (session environment: OS, web browser etc).

By default, the user session logging mechanism is not activated. The simplest way to activate logging is using the Enable Logging button on the Administration > User Session Log application screen. Alternatively, you can use cuba.UserSessionLogEnabled application property.

If needed, you can create a report for the sec$SessionLogEntry entity.

3.10. Functionality Extension

The platform enables extending and overriding the following aspects of its functionality in applications:

  • Extending entity attributes set.

  • Extending screens functionality.

  • Extending and overriding business logic contained in Spring beans.

Below is an example of the first two operations, illustrated by adding the "Address" field to the User entity of the platform security subsystem.

3.10.1. Extending an Entity

In the application project, derive an entity class from com.haulmont.cuba.security.entity.User and add the required attribute with the corresponding access methods:

@Entity(name = "sales_ExtUser")
@Extends(User.class)
public class ExtUser extends User {

    @Column(name = "ADDRESS", length = 100)
    private String address;

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }
}

The new name of the entity should be specified in the @Entity annotation. Since the parent entity does not declare the inheritance strategy, it is assumed to be SINGLE_TABLE by default. It means that the child entity will be stored in the same table as the parent one, and the @Table annotation is not required. Other parent entity annotations ( @NamePattern, @Listeners, etc.) are automatically applied to the child entity, but can be overridden in its class.

An important element of the new entity class is the @Extends annotation, which takes the parent class as a parameter. It enables creating a registry of child entities and forces the platform mechanisms to use them everywhere instead of the parent ones. The registry is implemented by the ExtendedEntities class, which is a Spring bean named cuba_ExtendedEntities, and is also accessible via the Metadata interface.

Add a localized name of the new attribute to the com.sample.sales.entity package:

messages.properties

ExtUser.address=Address

messages_ru.properties

ExtUser.address=Адрес

Register the new entity in the persistence.xml file of the project:

<class>com.sample.sales.entity.ExtUser</class>

Add the update script for the corresponding table to the database create and update scripts:

-- add column for "address" attribute
alter table SEC_USER add column ADDRESS varchar(100)
^
-- add discriminator column required for entity inheritance
alter table SEC_USER add column DTYPE varchar(100)
^
-- set discriminator value for existing records
update SEC_USER set DTYPE = 'sales_ExtUser' where DTYPE is null
^

In order to use new entity attributes in screens, create views for the new entity with the same names as the views of the base entity. A new view should extend the base view and define new attributes, for example:

<view class="com.sample.sales.entity.ExtUser"
      name="user.browse"
      extends="user.browse">

    <property name="address"/>
</view>

The extended view will include all attributes from its parent view. An extended view is not required if the base one extends _local and you add only local attributes, so in the described case this step can be omitted.

3.10.2. Extending Screens

The platform supports creating new XML descriptors by inheriting them from the existing ones.

XML inheritance is implemented by specifying the parent descriptor path in the extends attribute of the root window element.

XML screen elements overriding rules:

  • If the extending descriptor has a certain element, the corresponding element will be searched for in the parent descriptor using the following algorithm:

    • If the overriding element has the id attribute, the corresponding element with the same id will be searched for.

    • If the search is successful, the found element is overridden.

    • Otherwise, the platform determines how many elements with the provided path and name are contained in the parent descriptor. If there is only one element, it is overridden.

    • If the search yields no result and there is either zero or more than one element with the given path and name in the parent descriptor, a new element is added.

  • The text for the overridden or added element is copied from the extending element.

  • All attributes from the extending element are copied to the overridden or added element. If attribute names match, the value is taken from the extending element.

  • By default, the new element is added to the end of the list of adjacent elements. In order to add a new element to the beginning or with an arbitrary index, you can do the following:

    • Define an additional namespace in the extending descriptor: xmlns:ext="http://schemas.haulmont.com/cuba/window-ext.xsd".

    • Add the ext:index attribute with a desired index, for example: ext:index="0" to the extending element.

In order to debug the descriptor conversion, you can output the resulting XML to the server log by specifying the TRACE level for the com.haulmont.cuba.gui.xml.XmlInheritanceProcessor logger in the Logback configuration file.

Extending Legacy Screens

The framework contains a number of screens implemented with legacy API for backward compatibility. Below are examples of extending screens of the User entity from the security subsystem.

First, consider a browser screen of the ExtUser entity:

ext-user-browse.xml
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
        xmlns:ext="http://schemas.haulmont.com/cuba/window-ext.xsd"
        extends="/com/haulmont/cuba/gui/app/security/user/browse/user-browse.xml">
    <layout>
        <groupTable id="usersTable">
            <columns>
                <column ext:index="2" id="address"/>
            </columns>
        </groupTable>
    </layout>
</window>

In this example, the descriptor is inherited from the standard User entities browser of the framework. The address column is added to the table with index 2, so it is displayed after login and name.

If you register a new screen in screens.xml with the same identifiers that were used for the parent screen, the new screen will be invoked everywhere instead of the old one.

<screen id="sec$User.browse"
        template="com/sample/sales/gui/extuser/extuser-browse.xml"/>
<screen id="sec$User.lookup"
        template="com/sample/sales/gui/extuser/extuser-browse.xml"/>

Similarly, let’s create an edit screen:

ext-user-edit.xml
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
        xmlns:ext="http://schemas.haulmont.com/cuba/window-ext.xsd"
        extends="/com/haulmont/cuba/gui/app/security/user/edit/user-edit.xml">
    <layout>
        <groupBox id="propertiesBox">
            <grid id="propertiesGrid">
                <rows>
                    <row id="propertiesRow">
                        <fieldGroup id="fieldGroupLeft">
                            <column>
                                <field ext:index="3" id="address" property="address"/>
                            </column>
                        </fieldGroup>
                    </row>
                </rows>
            </grid>
        </groupBox>
    </layout>
</window>

Register it in screens.xml with the identifier of the parent screen:

<screen id="sec$User.edit"
        template="com/sample/sales/gui/extuser/extuser-edit.xml"/>

Once all the above-mentioned actions are completed, the application will use ExtUser with the corresponding screens instead of the standard User entity of the platform.

Screen controller can be extended by creating a new class that is inherited from the base screen controller. The class name is specified in the class attribute of the root element of the extending XML descriptor; the usual rules of inheriting XML described above will apply.

Extending screens using CUBA Studio

In this example, we will add an Excel button to the customer browser table by extending the screen for the Customer entity from the Customer Management component described in Example of Application Component.

  1. Create a new project in Studio and add the Customer Management component.

  2. Select Generic UI in the project tree and click New > Screen in the context menu. Then select the Extend an existing screen on the Screen Templates tab. In the Extend Screen list, select customer-browse.xml. The new ext-customer-browse.xml and ExtCustomerBrowse.java files will be created in the web module.

  3. Open the ext-customer-browse.xml and switch to the Designer tab. The components of the parent screen are displayed in the designer workspace.

  4. Select the customersTable and add a new excel action.

  5. Add a button to the buttonsPanel linked to the customersTable.excel action.

As a result, the ext-customer-browse.xml code on the Text tab will look as follows:

ext-customer-browse.xml
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd"
        messagesPack="com.company.sales2.web"
        extends="com/company/customers/web/customer/customer-browse.xml">
    <layout>
        <groupTable id="customersTable">
            <actions>
                <action id="excel" type="excel"/>
            </actions>
            <buttonsPanel id="buttonsPanel">
                <button id="excelButton" action="customersTable.excel"/>
            </buttonsPanel>
        </groupTable>
    </layout>
</window>

Consider the ExtCustomerBrowse screen controller.

ExtCustomerBrowse.java
@UiController("customers_Customer.browse")
@UiDescriptor("ext-customer-browse.xml")
public class ExtCustomerBrowse extends CustomerBrowse {
}

As the screen identifier customers_Customer.browse matches the identifier of a base screen, the new screen will be invoked everywhere instead of the old.

3.10.3. Extending Business Logic

The main part of platform business logic is contained in Spring beans. This enables to easily extend or override it in the application.

To substitute a bean implementation, you should create your own class that implements the interface or extends the base platform class and register it in spring.xml of the application. You cannot apply the @Component annotation to the extending class; overriding beans is possible only in the XML configuration.

Below is an example of adding a method to the PersistenceTools bean.

First, create a class with the necessary method:

public class ExtPersistenceTools extends PersistenceTools {

  public Entity reloadInSeparateTransaction(final Entity entity, final String... viewNames) {
      Entity result = persistence.createTransaction().execute(new Transaction.Callable<Entity>() {
          @Override
          public Entity call(EntityManager em) {
              return em.reload(entity, viewNames);
          }
      });
      return result;
  }
}

Register the class in spring.xml of the project core module with the same identifier as the platform bean:

<bean id="cuba_PersistenceTools" class="com.sample.sales.core.ExtPersistenceTools"/>

After that, the Spring context will always return ExtPersistenceTools instead of the base PersistenceTools instance. A checking code example:

Persistence persistence;
PersistenceTools tools;

persistence = AppBeans.get(Persistence.class);
tools = persistence.getTools();
assertTrue(tools instanceof ExtPersistenceTools);

tools = AppBeans.get(PersistenceTools.class);
assertTrue(tools instanceof ExtPersistenceTools);

tools = AppBeans.get(PersistenceTools.NAME);
assertTrue(tools instanceof ExtPersistenceTools);

The same logic can be used for overriding services, for example, from application components: to substitute a bean implementation, you should create a class that extends the base service functionality. In the example below the NewOrderServiceBean class is created to override the method from the base OrderServiceBean:

public class NewOrderServiceBean extends OrderServiceBean {
    @Override
    public BigDecimal calculateOrderAmount(Order order) {
        BigDecimal total = super.calculateOrderAmount(order);
        BigDecimal vatPercent = new BigDecimal(0.18);
        return total.multiply(BigDecimal.ONE.add(vatPercent));
    }
}

Then, if you register the new class in spring.xml, the new implementation will be used instead of the one from OrderServiceBean. Note that the base service id from the application component is used with the fully qualified name of the new class:

<bean id="workshop_OrderService" class="com.company.retail.service.NewOrderServiceBean"/>

3.10.4. Registration of Servlets and Filters

Servlets and filters defined in an application component have to be registered programmatically. Usually, servlets and filters are registered in the web.xml configuration file, however, the component’s web.xml has no effect in the target application.

The ServletRegistrationManager bean is designed for registering servlets and filters dynamically with the correct ClassLoader and enables using such static classes as AppContext. It also guarantees the correct work for all deployment options.

ServletRegistrationManager has two methods:

  1. createServlet() - creates a servlet of the given servlet class. It loads the servlet class with the correct ClassLoader that is obtained from the application context object. It means that a new servlet will be able to use static classes, for example, AppContext or Messages bean.

  2. createFilter() - creates filters in the same way.

In order to use this bean, we recommend creating an initializer bean in the application component. This bean should contain event listeners that are subscribed to ServletContextInitializedEvent and ServletContextDestroyedEvent.

For example:

@Component
public class WebInitializer {

    @Inject
    private ServletRegistrationManager servletRegistrationManager;

    @EventListener
    public void initializeHttpServlet(ServletContextInitializedEvent e) {
        Servlet myServlet = servletRegistrationManager.createServlet(
                e.getApplicationContext(), "com.demo.comp.MyHttpServlet");

        e.getSource().addServlet("my_servlet", myServlet)
                .addMapping("/myservlet/");
    }
}

Here, the WebInitializer class has only one event listener which is used to register an HTTP servlet from an application component in the target application.

The createServlet() method takes the application context obtained from ServletContextInitializedEvent and the HTTP servlet FQN. Then we register the servlet by its name (my_servlet) and define HTTP-mapping (/myservlet/). Now, if you add this app component to you application, MyHttpServlet will be registered right after the initialization of the servlet and application contexts.

If the application context is /app, and servlet is registered with mapping myservlet, it will be available at /app/myservlet.

For more complex example, see the Registering DispatcherServlet from Application Component section.

4. Application Development

This chapter contains practical information on how to create platform-based applications.

Code Formatting

  • For Java and Groovy code, it is recommended to follow the standard style described in Java Code Conventions. When programming in IntelliJ IDEA, you can just use the default style and Ctrl-Alt-L shortcut for formatting.

    The maximum line length is 120 characters. The indentation is 4 characters; using spaces instead of tabs is enabled.

  • XML code: indentation is 4 characters; using spaces instead of tabs is enabled.

Naming Conventions

Identifier Naming Rule Example

Java and Groovy classes

Screen controller class

UpperCamelCase

Browse screen controller − {EntityClass}Browse

Edit screen controller − {EntityClass}Edit.

CustomerBrowse

OrderEdit

XML screen descriptors

Component identifier, parameter names in queries

lowerCamelCase, only letters and numbers.

attributesTable

:component$relevantTo

:ds$attributesDs

Datasource identifier

lowerCamelCase, only letters and numbers ending with Ds.

attributesDs

SQL scripts

Reserved words

lowercase

create table

Tables

UPPER_CASE. The name is preceded by the project name to form a namespace. It is recommended to use singular form in table names.

SALES_CUSTOMER

Columns

UPPER_CASE

CUSTOMER

TOTAL_AMOUNT

Foreign key columns

UPPER_CASE. Consists of the table referred by the column (without the project prefix) and the _ID suffix.

CUSTOMER_ID

Indexes

UPPER_CASE. Consists of the IDX_ prefix, name of the table that the index is created for (with the project prefix) and names of the fields included in the index.

IDX_SALES_CUSTOMER_NAME

4.2. Project File Structure

Below is the project file structure of a simple application, Sales, consisting of the Middleware and Web Client blocks.

project structure
Figure 48. Project File Structure

The project root contains build scripts: build.gradle and settings.gradle.

The modules directory includes the subdirectories of the project default modulesglobal, core, web.

The global module contains the source code directory, src, with configuration files – metadata.xml, persistence.xml and views.xml. The com.sample.sales.service package contains interfaces of the Middleware services; the com.sample.sales.entity package contains entity classes and localization files for them.

project structure global
Figure 49. The global Module Structure

The core module contains the following directories:

project structure core
Figure 50. The core Module Structure

The web module contains the following directories:

project structure web
Figure 51. The web Module Structure

4.3. Build Scripts

Platform based projects are built using Gradle build system. Build scripts are two files in the project root directory:

  • settings.gradle – defines the project name and the set of modules.

  • build.gradle – defines the build configuration.

This section describes the structure of the scripts and the purpose and parameters of Gradle tasks.

4.3.1. Structure of build.gradle

This section describes the structure and main elements of the build.gradle script.

buildscript

The buildscript section of the script defines the following:

  • A version of the platform.

  • A set of repositories for loading project dependencies. See how to configure access to the repositories below.

  • Dependencies used by the build system, including the CUBA Gradle plugin.

Below the buildscript section, a few variables are defined. They are used in the script later.

cuba

The CUBA-specific build logic is encapsulated in the cuba Gradle plugin. It is included in the root of the script and in the configure section of all modules by the following statement:

apply(plugin: 'cuba')

The settings of the cuba plugin are defined in cuba section:

cuba {
    artifact {
        group = 'com.company.sales'
        version = '0.1'
        isSnapshot = true
    }
    tomcat {
        dir = "$project.rootDir/build/tomcat"
    }
    ide {
        copyright = '...'
        classComment = '...'
        vcs = 'Git'
    }
}

Let us consider the available options:

  • artifact - this section defines the group and version of the project artifacts being built. Artifact names are based on module names specified in settings.gradle.

    • group - artifact group.

    • version - artifact version.

    • isSnapshot - if true, artifact names will have the SNAPSHOT suffix.

      You can override the artifact version from the command line, for example:

      gradle assemble -Pcuba.artifact.version=1.1.1
  • tomcat - this section defines the settings of the Tomcat server which is used for fast deployment.

    • dir - location of the Tomcat installation directory.

    • port - listening port; 8080 by default.

    • debugPort - Java debug listening port; 8787 by default.

    • shutdownPort - port listening to the SHUTDOWN command; 8005 by default.

    • ajpPort - AJP connector port; 8009 by default.

  • ide - this section contains instructions for Studio and IDE.

    • vcs - a version control system for the project. Only Git and svn are currently supported.

    • copyright - copyright text to be inserted into beginning of each source file.

    • classComment - comment text to be placed above class declarations in Java source files.

  • uploadRepository - this section defines the settings of the repository where assembled project artifacts will be uploaded to upon completion of the uploadArchives task.

    • url - the repository URL. If not specified, Haulmont’s repository is used.

    • user - the repository user.

    • password - the repository password.

      You can pass the upload repository parameters from the command line with the following arguments:

      gradlew uploadArchives -PuploadUrl=http://myrepo.com/content/repositories/snapshots -PuploadUser=me -PuploadPassword=mypassword
dependencies

This section contains a set of application components used by the project. Components are specified by their global module artifact. In the following example, three components are used: com.haulmont.cuba (cuba component of the platform), com.haulmont.reports (reports premium add-on) and com.company.base (a custom component):

dependencies {
  appComponent("com.haulmont.cuba:cuba-global:$cubaVersion")
  appComponent("com.haulmont.reports:reports-global:$cubaVersion")
  appComponent("com.company.base:base-global:0.1-SNAPSHOT")
}
configure

The configure sections contain configuration of modules. The most important part of the configuration is the declaration of dependencies. For example:

configure(coreModule) {

    dependencies {
        // standard dependencies using variables defined in the script above
        compile(globalModule)
        provided(servletApi)
        jdbc(hsql)
        testRuntime(hsql)
        // add a custom repository-based dependency
        compile('com.company.foo:foo:1.0.0')
        // add a custom file-based dependency
        compile(files("${rootProject.projectDir}/lib/my-library-0.1.jar"))
        // add all JAR files in the directory to dependencies
        compile(fileTree(dir: 'libs', include: ['*.jar']))
    }

The entitiesEnhancing configuration block is used to configure the bytecode enhancement (weaving) of entity classes. It should be included at least in the global module, but can also be declared in each module separately.

Here, main and test are the sources sets for the projects and tests, and the optional persistenceConfig parameter enables specifying the set of persistence.xml files explicitly. If not set, the task will enhance all persistent entities listed in the *persistence.xml files located in the CLASSPATH.

configure(coreModule) {
    ...
    entitiesEnhancing {
        main {
            enabled = true
            persistenceConfig = 'custom-persistence.xml'
        }
        test {
            enabled = true
            persistenceConfig = 'test-persistence.xml'
        }
    }
}

Non-standard module dependencies can be specified in Studio in the Project properties section of CUBA project view.

In case of transitive dependencies and version conflicts, the Maven strategy of dependencies resolution will be used. According to it, the release versions have priority over the snapshot ones, and the more precise numeric qualifier is the newest. Other things being equal, the string qualifiers are prioritized in alphabetical order. For example:

1.0-beta1-SNAPSHOT         // the lowest priority
1.0-beta1
1.0-beta2-SNAPSHOT         |
1.0-rc1-SNAPSHOT           |
1.0-rc1                    |
1.0-SNAPSHOT               |
1.0                        |
1.0-sp                     V
1.0-whatever
1.0.1                      // the highest priority

4.3.2. Configuring Access to Repository

Main Repository

When you create a new project, you have to select a main repository containing CUBA artifacts. By default, there are two options (and you can have more if you set up a private repository):

  • https://repo.cuba-platform.com/content/groups/work - a repository located at Haulmont’s server. It requires common credentials which are specified right in the build script (cuba / cuba123).

  • https://dl.bintray.com/cuba-platform/main - a repository hosted at JFrog Bintray. It has anonymous access.

Both repositories have identical contents for the latest platform versions, but Bintray does not contain snapshots. We assume that Bintray is more reliable for worldwide access.

In case of Bintray, the build script of the new project is also configured to use Maven Central, JCenter and Vaadin Add-ons repositories separately.

Access to CUBA Premium Add-ons

Since version 7.0, the BPM, Charts, Full-Text Search and Reports add-ons have been made free and open-source. They are now located in the main repository described above, so you need to configure the premium repository only for using other premium add-ons, for example, WebDAV.

If your project uses CUBA Premium Add-ons, add one more repository to build.gradle:

  • If the main repository is repo.cuba-platform.com, add https://repo.cuba-platform.com/content/groups/premium

  • If the main repository is Bintray, add https://cuba-platform.bintray.com/premium

Example of adding https://repo.cuba-platform.com/content/groups/premium repository:

buildscript {
    // ...
    repositories {
        // ...
        maven {
            url 'https://repo.cuba-platform.com/content/groups/premium'
            credentials {
                username(rootProject.hasProperty('premiumRepoUser') ?
                        rootProject['premiumRepoUser'] : System.getenv('CUBA_PREMIUM_USER'))
                password(rootProject.hasProperty('premiumRepoPass') ?
                        rootProject['premiumRepoPass'] : System.getenv('CUBA_PREMIUM_PASSWORD'))
            }
        }
    }

Example of adding https://cuba-platform.bintray.com/premium repository:

buildscript {
    // ...
    repositories {
        // ...
        maven {
            url 'https://cuba-platform.bintray.com/premium'
            credentials {
                username(rootProject.hasProperty('bintrayPremiumRepoUser') ?
                        rootProject['bintrayPremiumRepoUser'] : System.getenv('CUBA_PREMIUM_USER'))
                password(rootProject.hasProperty('premiumRepoPass') ?
                        rootProject['premiumRepoPass'] : System.getenv('CUBA_PREMIUM_PASSWORD'))
            }
        }
    }

Both Premium Add-ons repositories require a user name and a password which are provided by per-developer subscription. The first part of your license key before dash is the repository user name, the part after dash is the password. For example, if your key is 111111222222-abcdefabcdef, then the user name is 111111222222 and the password is abcdefabcdef. In case of Bintray, the user name must be followed by @cuba-platform.

You can provide the credentials in one of the following ways.

  • The recommended way is to create a ~/.gradle/gradle.properties file in your user home directory and set properties in it:

    • Example of credentials for https://repo.cuba-platform.com/content/groups/premium repository:

      ~/.gradle/gradle.properties
      premiumRepoUser=111111222222
      premiumRepoPass=abcdefabcdef
    • Example of credentials for https://cuba-platform.bintray.com/premium repository:

      ~/.gradle/gradle.properties
      bintrayPremiumRepoUser=111111222222@cuba-platform
      premiumRepoPass=abcdefabcdef
  • Another way is to set the following environment variables for your operating system:

    • CUBA_PREMIUM_USER - will be used if premiumRepoUser is not set.

    • CUBA_PREMIUM_PASSWORD - will be used if premiumRepoPass is not set.

When you run the Gradle tasks from the command line, you can also pass the properties as command line arguments with the -P prefix, for example:

gradlew assemble -PpremiumRepoUser=111111222222 -PpremiumRepoPass=abcdefabcdef
Custom Repositories

Your project can use any number of custom repositories containing application components. They should be specified in build.gradle manually after the main repository, for example:

repositories {
    // main repository containing CUBA artifacts
    maven {
        url 'https://repo.cuba-platform.com/content/groups/work'
        credentials {
            // ...
        }
    }
    // custom repository
    maven {
        url 'http://localhost:8081/repository/maven-snapshots'
    }
}

4.3.3. Build Tasks

Tasks are executable units in Gradle. They are defined both in the plugins and in the build script itself. Below are CUBA-specific tasks; their parameters can be configured in build.gradle.

4.3.3.1. buildInfo

The buildInfo task is automatically added to your global module configuration by the CUBA Gradle plugin. It writes the build-info.properties file with the information about your application into the global artifact (e.g. app-global-1.0.0.jar). This information is read by the BuildInfo bean at runtime and is displayed on the Help > About window. This bean can also be invoked by different mechanisms to get the information about the application name, version, etc.

You can optionally change the following task parameters:

  • appName - application name. By default, the project name set in settings.gradle is used.

  • artifactGroup - artifact group, which is by convention equal to the root package of the project.

  • version - application version. By default, the version set in the cuba.artifact.version property is used.

  • properties - a map of arbitrary properties, empty by default.

Example of specifying custom properties of the buildInfo task:

configure(globalModule) {
    buildInfo {
        appName = 'MyApp'
        properties = ['prop1': 'val1', 'prop2': 'val2']
    }
    // ...
4.3.3.2. buildUberJar

buildUberJar – the task of the CubaUberJarBuilding type that creates JAR files containing the application code and all its dependencies together with embedded Jetty HTTP server. You can create either a single all-in-one JAR file or separate JARs for each application block, e.g. app-core.jar for the middleware and app.jar for the web client.

The task must be declared in the root of build.gradle. The resulting JAR files are located in the build/distributions project subdirectory. See the UberJAR Deployment section for how to run the generated JAR files.

The task can be configured using the Deployment > UberJAR settings page in Studio.

Task parameters:

  • coreJettyEnvPath - required parameter that defines a relative (from the project root) path to a file which contains JNDI resource definitions for Jetty HTTP server. The file must at least contain definition of a JDBC data source for the main database. Studio can generate this file on the basis of entered database connection parameters.

    task buildUberJar(type: CubaUberJarBuilding) {
        coreJettyEnvPath = 'modules/core/web/META-INF/jetty-env.xml'
        // ...
    }

    You can also provide different DB settings for one and the same UberJar at run time using different jetty-env.xml files and the -jettyEnvPath command line argument.

  • appProperties - a map defining application properties. These properties will be added to the WEB-INF/local.app.properties files inside generated JARs.

    task buildUberJar(type: CubaUberJarBuilding) {
        appProperties = ['cuba.automaticDatabaseUpdate' : true]
        // ...
    }
  • singleJar - if set to true, a single JAR containing all modules (core, web, portal) will be created. false by default.

    task buildUberJar(type: CubaUberJarBuilding) {
        singleJar = true
        // ...
    }
  • webPort - port for single (if singleJar=true) or web JAR embedded HTTP server, 8080 if not defined. Can also be set at run time using the -port command line argument.

  • corePort - port for core JAR embedded HTTP server, 8079 if not defined. Can also be set at run time using the -port command line argument for the respective JAR.

  • portalPort - port for portal JAR embedded HTTP server, 8081 if not defined. Can also be set at run time using the -port command line argument for the respective JAR.

  • appName - name of the application, which is app by default. You can change it for the whole project if you set Module prefix field in the Project Properties window in Studio, or you can set it only for the buildUberJar task using this parameter. For example:

    task buildUberJar(type: CubaUberJarBuilding) {
        appName = 'sales'
        // ...
    }

    After changing the application name to sales the task will generate sales-core.jar and sales.jar files and the web client will be available at http://localhost:8080/sales. You can also change web contexts at run time without changing the application name using the -contextName command line argument or just by renaming the JAR file itself.

  • logbackConfigurationFile - defines a relative path to a file to be used for logging configuration.

    For example:

    logbackConfigurationFile = "/modules/global/src/logback.xml"
  • useDefaultLogbackConfiguration - while true (default value), the task will copy its own standard logback.xml configuration file.

  • webJettyConfPath - a relative path to a file to be used for Jetty server configuration for the single (if singleJar=true) or web JAR (if singleJar=false). See https://www.eclipse.org/jetty/documentation/9.4.x/jetty-xml-config.html.

  • coreJettyConfPath (do not confuse with coreJettyEnvPath described above) - a relative path to a file to be used for Jetty server configuration for the core JAR (if singleJar=false).

  • portalJettyConfPath - a relative path to a file to be used for Jetty server configuration for the portal JAR (if singleJar=false).

  • coreWebXmlPath - a relative path to a file to be used as a web.xml for the core module web application.

  • webWebXmlPath - a relative path to a file to be used as a web.xml for the web module web application.

  • portalWebXmlPath - a relative path to a file to be used as a web.xml for the portal module web application.

  • excludeResources - a file pattern of resources to not include in JARs.

  • mergeResources - a file pattern of resources to be merged in JARs.

  • webContentExclude - a file pattern of web content to not include in web JAR.

  • coreProject - a Gradle project representing the core module (Middleware). If not defined, the standard core module is used.

  • webProject - a Gradle project representing the web module (Web Client). If not defined, the standard web module is used.

  • portalProject - a Gradle project representing the portal module (Web Portal). If not defined, the standard portal module is used.

  • frontProject - a Gradle project representing the Polymer UI module. If not defined, the standard polymer-client module is used.

  • polymerBuildDir - the name of the directory where the Polymer UI is built. It is es6-unbundled by default. Set this parameter if you have changed the build preset in polymer.json.

4.3.3.3. buildWar

buildWar – the task of the CubaWarBuilding type, which builds a WAR file from the application code and its dependencies. It should be declared in the root of build.gradle. The resulting WAR file(s) are located in the build/distributions project subdirectory.

The task can be configured using the Deployment > WAR Settings page in Studio.

Any CUBA application consists of at least two blocks: Middleware and Web Client. So the most natural way to deploy an application is to create two separate WAR files: one for Middleware and one for Web Client. This also allows you to scale your application when the number of users grows. However, separate WAR files contain some duplicated dependencies that increase overall size. Besides, extended deployment options are often not needed and rather complicate the process. The CubaWarBuilding task can create both types of WAR files: one per block or single WAR containing both blocks. In the latter case, the application blocks are loaded into separate class loaders inside one web application.

Creating separate WAR files for Middleware and Web Client

To create separate WAR files for Middleware and Web Client, use the following task configuration:

task buildWar(type: CubaWarBuilding) {
    appHome = '${app.home}'
    appProperties = ['cuba.automaticDatabaseUpdate': 'true']
    singleWar = false
}

Task parameters:

  • appName - the name of the web application. By default, it corresponds to the Modules prefix, e.g. app.

  • appHome – the path to the application home directory. You can specify an absolute or relative path to the home directory, or a placeholder for Java system variable which should be set at server start.

  • appProperties - an optional map defining application properties. These properties will be added to the /WEB-INF/local.app.properties files inside generated WAR.

    appProperties = ['cuba.automaticDatabaseUpdate': 'true'] will create the database at the first launch, if there wasn’t any.

  • singleWar - should be set to false for building separate WAR files.

  • includeJdbcDriver - include JDBC driver which is currently used in the project. false by default.

  • includeContextXml - include Tomcat context.xml file which is currently used in the project. false by default.

  • coreContextXmlPath - the relative path to a file which should be used instead of project’s context.xml if includeContextXml is set to true.

  • hsqlInProcess - if set to true, the database URL in context.xml will be modified for HSQL in-process mode.

  • coreProject - the Gradle project representing the core module (Middleware). If not defined, the standard core module is used.

  • webProject - the Gradle project representing the web module (Web Client). If not defined, the standard web module is used.

  • portalProject - the Gradle project representing the portal module (Web Portal). Set this property if the application project contains the portal module. For example, portalProject = project(':app-portal').

  • coreWebXmlPath, webWebXmlPath, portalWebXmlPath - a relative path to a file to be used as a web.xml of the corresponding application block.

    Example of using custom web.xml files:

    task buildWar(type: CubaWarBuilding) {
        singleWar = false
        // ...
        coreWebXmlPath = 'modules/core/web/WEB-INF/production-web.xml'
        webWebXmlPath = 'modules/web/web/WEB-INF/production-web.xml'
    }
  • logbackConfigurationFile - defines a relative path to a file to be used for logging configuration.

    For example:

    logbackConfigurationFile = "/modules/global/src/logback.xml"
  • useDefaultLogbackConfiguration - while true (default value), the task will copy its own standard logback.xml configuration file.

  • polymerBuildDir - the name of the directory where the Polymer UI is built. It is es6-unbundled by default. Set this parameter if you have changed the build preset in polymer.json.

Creating a single WAR file

To create a single WAR file that comprises both Middleware and Web Client blocks, use the following task configuration:

task buildWar(type: CubaWarBuilding) {
    appHome = '${app.home}'
    webXmlPath = 'modules/web/web/WEB-INF/single-war-web.xml'
}

The following parameters should be specified in addition to the ones described above:

  • singleWar - should be omitted or set to true.

  • webXmlPath - the relative path to a file to be used as a web.xml of the single WAR. This file defines two servlet context listeners that load the application blocks: SingleAppCoreServletListener and SingleAppWebServletListener. All initialization parameters are passed to them through context parameters.

    Example of single-war-web.xml:

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns="http://java.sun.com/xml/ns/javaee"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
             version="3.0">
    
        <!--Application components-->
        <context-param>
            <param-name>appComponents</param-name>
            <param-value>com.haulmont.cuba</param-value>
        </context-param>
    
        <!-- Web Client parameters -->
    
        <context-param>
            <description>List of app properties files for Web Client</description>
            <param-name>appPropertiesConfigWeb</param-name>
            <param-value>
                classpath:com/company/sample/web-app.properties
                /WEB-INF/local.app.properties
            </param-value>
        </context-param>
    
        <context-param>
            <description>Web resources version for correct caching in browser</description>
            <param-name>webResourcesTs</param-name>
            <param-value>${webResourcesTs}</param-value>
        </context-param>
    
        <!-- Middleware parameters -->
    
        <context-param>
            <description>List of app properties files for Middleware</description>
            <param-name>appPropertiesConfigCore</param-name>
            <param-value>
                classpath:com/company/sample/app.properties
                /WEB-INF/local.app.properties
            </param-value>
        </context-param>
    
        <!-- Servlet context listeners that load the application blocks -->
    
        <listener>
            <listener-class>
                com.vaadin.server.communication.JSR356WebsocketInitializer
            </listener-class>
        </listener>
        <listener>
            <listener-class>
                com.haulmont.cuba.core.sys.singleapp.SingleAppCoreServletListener
            </listener-class>
        </listener>
        <listener>
            <listener-class>
                com.haulmont.cuba.web.sys.singleapp.SingleAppWebServletListener
            </listener-class>
        </listener>
    </web-app>

All filters and servlets for single WAR deployment should be registered programmatically, see Registration of Servlets and Filters.

Single WAR contains only core and web modules (Middleware and Web Client). To deploy the portal module, use separate WAR files.

See also WAR deployment to Jetty section for step-by-step instructions on some variants of WAR deployment.

4.3.3.4. buildWidgetSet

buildWidgetSet - the task of the CubaWidgetSetBuilding which builds a custom GWT widgetset if the web-toolkit module exists in the project. This module enables development of custom visual components.

Available parameters:

  • style - the script output style: OBF, PRETTY or DETAILED. OBF by default.

  • logLevel - the logging level: ERROR, WARN, INFO, TRACE, DEBUG, SPAM, or ALL. INFO by default.

  • draft - compile quickly with minimal optimizations. false by default.

Example usage:

task buildWidgetSet(type: CubaWidgetSetBuilding) {
    widgetSetClass = 'com.company.sample.web.toolkit.ui.AppWidgetSet'
    style = 'PRETTY'
}
4.3.3.5. createDb

createDb – the task of the CubaDbCreation type which creates application database by executing the corresponding scripts. It is declared in the core module. Parameters:

  • dbms – the DBMS type, specified as the string hsql, postgres, mssql, or oracle.

  • dbName – the database name.

  • dbUser – the DBMS username.

  • dbPassword – the DBMS user password.

  • host – the DBMS host and port (optional) in the host[:port] format. If not specified, localhost is used.

  • connectionParams - an optional connection parameters string which will be appended to the end of the connection URL.

  • masterUrl – the URL used to connect when creating the database. If not specified, the default value that depends on the DBMS type and the host parameter is used.

  • dropDbSql – the SQL command to delete the database. If not specified, the default value that depends on the DBMS type is used.

  • createDbSql – the SQL command to create a database. If not specified, the default value that depends on the DBMS type is used.

  • driverClasspath – the list of JAR files containing the JDBC driver. The items in the list are separated by ":" on Linux and by ";" on Windows. If not specified, the system uses the dependencies that are part of the current module’s jdbc configuration. Explicit definition of driverClasspath is necessary when using Oracle, because its JDBC driver is not available in the dependencies.

  • oracleSystemPassword – the SYSTEM user password for Oracle.

Example for PostgreSQL:

task createDb(dependsOn: assemble, description: 'Creates local database', type: CubaDbCreation) {
    dbms = 'postgres'
    dbName = 'sales'
    dbUser = 'cuba'
    dbPassword = 'cuba'
}

Example for MS SQL Server:

task createDb(dependsOn: assemble, description: 'Creates local database', type: CubaDbCreation) {
    dbms = 'mssql'
    dbName = 'sales'
    dbUser = 'sa'
    dbPassword = 'saPass1'
    connectionParams = ';instance=myinstance'
}

Example for Oracle:

task createDb(dependsOn: assemble, description: 'Creates database', type: CubaDbCreation) {
    dbms = 'oracle'
    host = '192.168.1.10'
    dbName = 'orcl'
    dbUser = 'sales'
    dbPassword = 'sales'
    oracleSystemPassword = 'manager'
    driverClasspath = "$tomcatDir/lib/ojdbc6.jar"
}
4.3.3.6. debugWidgetSet

debugWidgetSet - the task of the CubaWidgetSetDebug type which launches GWT Code Server for debugging widgets in the browser.

Example usage:

task debugWidgetSet(type: CubaWidgetSetDebug) {
    widgetSetClass = 'com.company.sample.web.toolkit.ui.AppWidgetSet'
}

Ensure that the web-toolkit module has a dependency on Servlet API library in the runtime configuration:

configure(webToolkitModule) {
    dependencies {
        runtime(servletApi)
    }
...

See Debugging Web Widgets for information on how to debug code in the browser.

4.3.3.7. deploy

deploy – the task of the CubaDeployment type which performs fast deployment of a module to Tomcat. It is declared in the core, web and portal modules. Parameters:

  • appName – name of the web application that will be created from the module. In fact, it is the name of a subdirectory inside tomcat/webapps.

  • jarNames – the list of JAR file names (without versions) produced as a result of building a module and intended to be placed into the WEB-INF/lib catalog of the web application. All other module artifacts and dependencies will be copied to tomcat/shared/lib.

For example:

task deploy(dependsOn: assemble, type: CubaDeployment) {
    appName = 'app-core'
    jarNames = ['cuba-global', 'cuba-core', 'app-global', 'app-core']
}
4.3.3.8. deployThemes

deployThemes - the task of the CubaDeployThemeTask type which builds and deploys themes defined in the project to the running web application deployed by the deploy task. Changes in the themes are applied without the server restart.

For example:

task deployThemes(type: CubaDeployThemeTask, dependsOn: buildScssThemes) {
}
4.3.3.9. deployWar

deployWar - the task of the CubaJelasticDeploy type which deploys the WAR file to Jelastic server.

For example:

task deployWar(type: CubaJelasticDeploy, dependsOn: buildWar) {
   email = '<your@email.address>'
   password = '<your password>'
   context = '<app contex>'
   environment = '<environment name or ID>'
   hostUrl = '<Host API url>'
}

Task parameters:

  • appName - the name of the web application. By default, it corresponds to the Modules prefix, e.g., app.

  • email - Jelastic server account login.

  • password - Jelastic account password.

  • context - the application context. Default value: ROOT.

  • environment - the environment where the application WAR will be deployed. You can set either the environment name or its ID.

  • hostUrl - URL of the API host. Typically it is app.jelastic.<host name>.

  • srcDir - the directory where the WAR is located. By default it is "${project.buildDir}/distributions/war".

4.3.3.10. restart

restart – the task that stops the local Tomcat server, runs fast deployment, and starts the server once again.

4.3.3.11. setupTomcat

setupTomcat – the task of the CubaSetupTomcat type which performs installation and initialization of the local Tomcat server for subsequent fast deployment of the application. This task is automatically added to the project when you apply the cuba Gradle plugin, so you don’t need to declare it in build.gradle. Tomcat installation directory is specified by the tomcat.dir property of the cuba section. By default, it is the project’s build/tomcat subdirectory.

4.3.3.12. start

start – the task of the CubaStartTomcat type which starts the local Tomcat server installed by the setupTomcat task. This task is automatically added to the project when you add the cuba plugin, so you don’t need to declare it in build.gradle.

4.3.3.13. startDb

startDb – the task of the CubaHsqlStart type which starts the local HSQLDB server. Parameters:

  • dbName – database name, default is cubadb.

  • dbDataDir – database directory, default is the deploy/hsqldb subfolder of the project.

  • dbPort – server port, default is 9001.

For example:

task startDb(type: CubaHsqlStart) {
    dbName = 'sales'
}
4.3.3.14. stop

stop – the task of CubaStopTomcat type which stops the local Tomcat server installed by the setupTomcat task. This task is automatically added to the project when you include the cuba plugin, so you don’t need to declare it in build.gradle.

4.3.3.15. stopDb

stopDb – the task of the CubaHsqlStop type which stops the local HSQLDB server. The parameters are similar to startDb.

4.3.3.16. tomcat

tomcat – the task of the Exec type which starts the local Tomcat server in the opened terminal window and keeps it open even if the start failed. This task may be useful for troubleshooting, e.g., to detect problems caused by Java version mismatch etc, on the server start.

4.3.3.17. updateDb

updateDb – the task of the CubaDbUpdate type which updates the database by executing the corresponding scripts. It is similar to the createDb task, except that the dropDbSql and createDbSql parameters are omitted.

4.3.3.18. zipProject

zipProject is the task of the CubaZipProject type which creates a ZIP archive of your project. The archive will not contain IDE project files, build results and Tomcat server. But HSQL database is included to the archive if present in the build directory.

This task is automatically added to the project when you apply the cuba Gradle plugin, so you don’t need to declare it in build.gradle.

4.3.4. Starting Build Tasks

Gradle tasks described in build scripts can be launched in the following ways:

  • If you are working with the project in CUBA Studio, many commands that you run from the CUBA main menu actually delegate to Gradle tasks: all commands of the Build Tasks item, as well as Start/Stop/Restart Application Server and Create/Update Database commands.

  • Alternatively, you can use the executable gradlew script (Gradle wrapper) included in the project.

  • One more way is to use the manually installed Gradle version 4.10.3. In this case, run the gradle executable located in the bin subdirectory of the Gradle installation.

For example, in order to compile the Java files and build the JAR files for project artifacts, you need to run the following command:

Windows:
gradlew assemble
Linux & macOS:
./gradlew assemble

If your project uses Premium Add-ons and you are starting build tasks outside Studio, you should pass the Premium Add-ons repository credentials to Gradle. See the section above for details.

Typical build tasks in their normal usage sequence are provided below.

  • assemble – compile Java files and build JARs for project artifacts in the build subdirectories of the modules.

  • clean – remove build subdirectories of all project modules.

  • setupTomcat – setup the Tomcat server to the path that is specified by the cuba.tomcat.dir property of the build.gradle script.

  • deploy – deploy the application to the Tomcat server that has been pre-installed by the setupTomcat task.

  • createDb – create an application database and run the corresponding scripts.

  • updateDb – update the existing application database by running the corresponding scripts.

  • start – start the Tomcat server.

  • stop – stop the running Tomcat server.

  • restart – sequentially run the stop, deploy, start tasks.

4.3.5. Setting Up a Private Artifact Repository

This section describes how to set up a private Maven repository and use it instead of the CUBA public repository for storing the platform artifacts and other dependencies. It is recommended in the following cases:

  • You have an unstable or slow connection to the internet. In spite of the fact that Gradle caches downloaded artifacts on the developer’s machine, you may need to connect to the artifact repository from time to time, for example when you run build for the first time or switch to a newer version of the platform.

  • You cannot have direct access to the internet due to a security policy of your organization.

  • You are not going to prolong your subscription to CUBA Premium Add-ons, but you need to be able to build your application in the future using the downloaded version of the artifacts.

The process of setting up a private repository consists of the following steps:

  • Install the repository manager software in a network connected to the internet.

  • Configure the private repository as a proxy for the CUBA public repository.

  • Make your project build script use the private repository. It can be done in Studio or right in build.gradle.

  • Perform full build of your project to cache all required artifacts in the private repository.

4.3.5.1. Install the Repository Manager

For the purpose of this example, we will use Sonatype Nexus OSS repository manager.

On Microsoft Windows operating system
  • Download Sonatype Nexus OSS version 2.x (2.14.3 has been tested)

  • Unpack zip file to the directory c:\nexus-2.14.3-02

  • Modify settings located in file c:\nexus-2.14.3-02\conf\nexus.properties:

    • You may configure server port; default is 8081

    • Configure repository data folder:

      replace

      nexus-work=${bundleBasedir}/../sonatype-work/nexus

      with any convenient path to cached data, for example

      nexus-work=${bundleBasedir}/nexus/sonatype-work/content
  • Navigate to the folder c:\nexus-2.14.3-02\bin

  • To start and stop Nexus as a service, install the wrapper (run command as Administrator):

    nexus.bat install
  • Launch nexus service.

  • Open http://localhost:8081/nexus in the web browser and log in with the default credentials: login admin and password admin123.

With Docker

Alternatively, we can use Docker to simplify the setup for local use. Instructions are also available at the Docker Hub.

  • Run docker pull sonatype/nexus:oss to download the latest stable OSS image

  • Then build the container with docker run -d -p 8081:8081 --name nexus sonatype/nexus:oss

  • The docker container will have nexus running in a few minutes. Test by either way:

    • curl http://localhost:8081/nexus/service/local/status

    • Navigate to http://localhost:8081/nexus in the web browser.

  • Credentials are the same: login admin and password admin123.

4.3.5.2. Configure the Proxy Repository

Click to the Repositories link on the left panel.

On the opened Repositories page click the Add button, then choose Proxy Repository. A new repository will be added. Fill in required fields at Configuration tab:

  • Repository ID: cuba-work

  • Repository Name: cuba-work

  • Provider: Maven2

  • Remote Storage Location: https://repo.cuba-platform.com/content/groups/work

  • Auto Blocking Enabled: false

  • Enable Authentication, set Username: cuba, Password: cuba123

  • Click Save button.

Create a Repository Group, in Nexus click Add button, then choose Repository Group and do the following on Configuration tab:

  • Enter the Group ID: cuba-group

  • Enter the Group Name: cuba-group

  • Provider: Maven2

  • Add the repository cuba-work from Available Repositories to Ordered Group Repositories

  • Click Save button.

If you have a subscription to the Premium Add-ons, add one more repository with the following settings:

  • Repository ID: cuba-premium

  • Repository Name: cuba-premium

  • Provider: Maven2

  • Remote Storage Location: https://repo.cuba-platform.com/content/groups/premium

  • Auto Blocking Enabled: false

  • Enable Authentication, set the first part of your license key (before dash) in the Username field and the second part of your license key (after dash) in the Password field.

  • Click Save button.

  • Click Refresh button.

  • Select the cuba-group group.

  • On the Configuration tab, add cuba-premium repository to the group below cuba-work.

  • Click Save button.

4.3.5.3. Using the Private Repository

Now your private repository is ready to use. Find the cuba-group URL at the top of the screen, for example:

http://localhost:8081/nexus/content/groups/cuba-group
  • Find the list of registered repositories in Studio. If you are creating a new project, it is in the New Project window. For an existing project, open CUBA > Project Properties window.

  • In the new repository dialog, enter the repository URL and credentials: admin / admin123.

  • After saving the repository information, use it in the project by selecting its checkbox in the list.

  • Save the project properties or continue with the project creation wizard.

During the first build your new repository downloads necessary artifacts and keeps them in the cache for the next usage. You may find them in c:\nexus-2.14.3-02\sonatype-work folder.

4.3.5.4. Repository in an Isolated Network

If you need to develop on CUBA in a network without connection to the internet, do the following:

  • Install a copy of the repository manager in the target network.

  • Copy the cached content of the repository from the open network to the isolated one. If you followed the instructions above, the content is stored in

    c:\nexus-2.14.3-02\sonatype-work
  • Restart the nexus service.

If you need to add artifacts of a new platform version to the isolated repository, go to the environment connected to the internet, make a build through its repository and then copy the contents to the isolated environment again.

4.4. Creating a Project

The recommended way to create a new project is to use CUBA Studio. An example can be found in the Quick Start chapter of this manual.

Another option is to use CUBA CLI:

  1. Open a terminal and start CUBA CLI.

  2. Input the command create-app. You can use tab auto-completion.

  3. CLI will ask you for the project configuration. Click ENTER to accept the defaults or select another options:

    • Project name – the project name. For sample projects CLI generates random names that can be selected by default.

    • 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.

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

    • Root package – the root package of Java classes.

    • Database – the SQL database to use.

After that, the empty project will be created in a new folder in the current directory. You can keep developing it in Studio or using CLI and any IDE.

4.5. Working with 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.

Application components published on CUBA Marketplace are called add-ons, because they extend functionality of the framework and can be used in any CUBA-based application.

4.5.1. Using Public Add-ons

An add-on published on Marketplace can be added to your project in one of the ways described below. The first and the second approaches assume that you use one of the standard CUBA repositories. The last approach is applicable for open-source add-ons and doesn’t involve any remote repositories.

By Studio

If you are using CUBA Studio 11+, manage add-ons via CUBA Add-Ons window as described in the Studio documentation.

For the previous CUBA Studio versions, follow the steps below:

  1. Edit Project properties and on the App components panel click the plus button next to Custom components.

  2. Copy add-on coordinates from the marketplace page or from the add-on’s documentation and paste them in the coordinates field, for example:

    com.haulmont.addon.cubajm:cuba-jm-global:0.3.1
  3. Click OK in the dialog. Studio will try to find the add-on binaries in the repository currently selected for the project. If it is found, the dialog will close and the add-on will appear in the list of custom components.

  4. Save the project properties by clicking OK.

By manual editing
  1. Edit build.gradle and specify the add-on coordinates in the root dependencies section:

    dependencies {
        appComponent("com.haulmont.cuba:cuba-global:$cubaVersion")
        // your add-ons go here
        appComponent("com.haulmont.addon.cubajm:cuba-jm-global:0.3.1")
    }
  2. Execute gradlew idea in the command line to include add-on in your project’s development environment.

  3. Edit web.xml files of the core and web modules and add the add-on identifier (which is equal to Maven groupId) to the space-separated list of application components in the appComponents context parameter:

    <context-param>
        <param-name>appComponents</param-name>
        <param-value>com.haulmont.cuba com.haulmont.addon.cubajm</param-value>
    </context-param>
By building from sources
  1. Clone the add-on’s repository to a local directory and import the project into Studio.

  2. Execute CUBA > Advanced > Install app component main menu command to install the add-on to the local Maven repository (by default it is ~/.m2 directory).

  3. Open your project in Studio and add the local Maven repository to the repositories list in Project > Properties.

  4. Install the add-on into the project using the CUBA Add-ons dialog in Studio. For details, see Installing add-on by coordinates in the Managing Add-ons section of the Studio User Guide.

  5. Click OK in the dialog and save the project properties.

If a project uses more than one add-ons having a web-toolkit module, then the project must have web-toolkit module too. If not, then only one widgetset from one add-on is loaded in the application. So the web-toolkit module is needed to integrate all widgetsets from used add-ons.

4.5.2. Creating Application Components

This section contains some recommendations useful if you are developing a reusable application component.

Naming rules
  1. Choose the root package using the standard reverse-DNS notation, e.g. com.jupiter.amazingsearch.

    Root package should not begin with a root package of any other component or application. For example, if you have an application with com.jupiter.tickets root package, you cannot use com.jupiter.tickets.amazingsearch package for a component. The reason is that Spring scans the classpath for the beans starting from the specified root package, and this scanning space must be unique for each component.

  2. Namespace is used as a prefix for the database tables, so for a public component it should be composite, like jptams, not just search. It will minimize the risk of name collisions in the target application. You cannot use underscores and dashes in namespace, only letters and digits.

  3. Module prefix should repeat namespace, but can contain dashes, like jpt-amsearch.

  4. Use namespace as a prefix for bean names and application properties, for example:

    @Component("jptams_Finder")
    @Property("jptams.ignoreCase")
Installing into the local Maven repository

In order to make the component available to the projects located on your computer, install it into the local Maven repository by executing the CUBA > Advanced > Install app component menu command. This command just runs the install Gradle task after stopping Gradle daemons.

Uploading to a remote Maven repository
  1. Set up a repository as explained in Setting Up a Private Artifact Repository.

  2. Specify your repository and credentials for the project instead of the standard CUBA repository.

  3. Open build.gradle of the component project in a text editor and add uploadRepository to the cuba section:

    cuba {
        //...
        // repository for uploading your artifacts
        uploadRepository {
            url = 'http://repo.company.com/nexus/content/repositories/snapshots'
            user = 'admin'
            password = 'admin123'
        }
    }
  4. Open the component project in Studio.

  5. Run the uploadArchives Gradle task from the command line. The component’s artifacts will be uploaded to your repository.

  6. Remove the component artifacts from your local Maven repository to ensure that they will be downloaded from the remote repository during the next assembling of the application project: just delete the .m2/repository/com/company folder located in your user home directory.

  7. Now, when you assemble and run the application that uses this component, it will be downloaded from the remote repository.

Uploading to Bintray
  1. Register at https://bintray.com/signup/oss

    You can use social login (GitHub, Gmail, Twitter) on Bintray, but later you will have to reset your password, as this account’s password is required for getting an API-key (see below).

  2. Get the Bintray user name. It can be found in the URL you see after login to Bintray. For example, in https://bintray.com/vividphoenix the vividphoenix is the user name.

  3. Get the API-key. It can be found in Bintray interface if you edit your profile. In the API-key section, you will be asked to input your account password to obtain the key. Then you will be able to use this key and the username for plugin authentication:

    • The Bintray credentials can be added as environment variables:

      BINTRAY_USER=your_bintray_user
      BINTRAY_API_KEY=9696c1cb90752357ded8fdf20eb3fa921bf9dbbb
    • Instead of environment variables, you can explicitly define these parameters in the build.gradle file of the project:

      bintray {
          user = 'bintray_user'
          key = 'bintray_api_key'
          ...
      }
    • Alternatively, you can provide Bintray credentials in the command line:

      ./gradlew clean assemble bintrayUpload -Pcuba.artifact.version=1.0.0 -PbintrayUser=your_bintray_user -PbintrayApiKey=9696c1cb90752357ded8fdf20eb3fa921bf9dbbb
  4. Create a public repository of Maven type. Setting a license type is mandatory for open source (OSS) repositories.

    Bintray structure implies using packages inside the repositories. At this stage, creating a package is not mandatory, as it will be automatically created while running the gradle bintrayUpload task.

  5. In your build.gradle, add the Bintray upload plugin dependency as follows:

    buildscript {
        // ...
        dependencies {
            classpath "com.haulmont.gradle:cuba-plugin:$cubaVersion"
            // Bintray upload plugin
            classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.0"
        }
    }
  6. At the end of build.gradle, add the Bintray plugin settings:

    /** * If you have a multi-project build, make sure to apply the plugin and the plugin configuration to every project which artifacts you want to publish to Bintray. */
    subprojects {
        apply plugin: 'com.jfrog.bintray'
    
        bintray {
            user = project.hasProperty('bintrayUser') ? project.property('bintrayUser') : System.getenv('BINTRAY_USER')
            key = project.hasProperty('bintrayApiKey') ? project.property('bintrayApiKey') : System.getenv('BINTRAY_API_KEY')
    
            configurations = ['archives']
    
            // make files public ?
            publish = true
            // override existing artifacts?
            override = false
    
            // metadata
            pkg {
                repo = 'main'           // your repository name
                name = 'amazingsearch'  // package name - it will be created upon upload
                desc = 'AmasingSearch'  // optional package description
    
                // organization name, if your repository is created inside an organization.
                // remove this parameter if you don't have an organization
                userOrg = 'jupiter-org'
    
                websiteUrl = 'https://github.com/jupiter/amazing-search'
                issueTrackerUrl = 'https://github.com/jupiter/amazing-search/issues'
                vcsUrl = 'https://github.com/jupiter/amazing-search.git' // mandatory for Open Source projects
    
                licenses = ["Apache-2.0"]
                labels = ['cuba-platform', 'opensource']
    
                //githubRepo = 'amazingsearch/cuba-platform' // optional Github repository
                //githubReleaseNotesFile = 'README.md' // optional Github readme file
            }
        }
    }
    • here, pkg:repo is your repository (use main),

    • pkg:name is the package name (use your unique name, as amazingsearch),

    • pkg:desc is the optional package description that will be shown on Bintray interface,

    • and pkg:userOrg - is the name of an organization the repo belongs to (if not set, the BINTRAY_USER will be used as the organization name by default).

  7. Now you can build and upload the project with the following command:

    ./gradlew clean assemble bintrayUpload -Pcuba.artifact.version=1.0.0
  8. If you publish the add-on on the CUBA Marketplace, its repository will be linked to the standard CUBA repositories and users won’t have to specify your repository in their projects.

4.5.3. Example of Application Component

In this section, we’ll consider a complete 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 namecustomers

    • Project namespacecust

    • Root packagecom.company.customers

  2. Open the Project Properties window and set the Module prefix to cust.

  3. Create the Customer entity with at least the name attribute.

    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.

  5. Go to the menu designer and rename the application-cust menu item to customerManagement. Then, open the messages.properties file in the Main Message Pack section and specify the new caption for the customerManagement menu item.

  6. Generate app-component.xml descriptor by clicking the CUBA > Advanced > App Component Descriptor item in the main menu.

  7. Test the Customer Management functionality:

    • Select CUBA > Create Database in the main menu.

    • Run the application: click the debug button next to the selected CUBA Application configuration in the main toolbar.

    • Open http://localhost:8080/cust in your web browser.

  8. Install the application component into the local Maven repository by selecting the CUBA > Advanced > Install App Component menu item.

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

    • Project namesales

    • Project namespacesales

    • Root packagecom.company.sales

  2. Open the Project Properties window and select Use local Maven repository checkbox.

  3. Include application component in the project as described in the Installing add-on by coordinates section of the Studio User Guide. Use Maven coordinates of the Customer Management component, e.g. com.company.customers:cust-global:0.1-SNAPSHOT.

  4. 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.

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

  6. Test the application functionality:

    • Select CUBA > Create Database in the main menu.

    • Run the application: click the debug button next to the selected CUBA Application configuration in the main toolbar.

    • 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. Include this attribute to both browser and editor screens.

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

  4. Test the changes in the component:

    • Select CUBA > Update Database in the main menu.

    • Run the application: click the debug button next to the selected CUBA Application configuration in the main toolbar.

    • Open http://localhost:8080/cust in your web browser.

  5. Re-install the application component into the local Maven repository by selecting the CUBA > Advanced > Install App Component menu item.

  6. Switch to the sales project in Studio.

  7. Select CUBA > Build Tasks > Clean.

  8. Select CUBA > Update Database in the main menu – the update script from the Customer Management component will be executed.

  9. Run the application: click the debug button next to the selected CUBA Application configuration in the main toolbar.

  10. Open http://localhost:8080/app in your web browser – the application will contain the Customer entity and screens with the new address attribute.

4.5.4. Additional Data Stores in Application Component

If an application component uses an additional data store, the application must define a data store with the same name and of the same type. For example, if the component uses db1 data store connected to a PostgreSQL database, the application must have the db1 data store of PostgreSQL type too.

If you are using Studio, just create the additional data store as described in the Studio documentation. Otherwise, use the instructions from the Data Stores section.

4.5.5. Registering DispatcherServlet from Application Component

In this section you will learn how to propagate the servlets and filters configuration from an application component to the owning application. To avoid the duplication of code in the web.xml file, you need to register your servlets and filters in the component using the special ServletRegistrationManager bean.

The most common case of servlets registration is described through the example of HTTP servlet registration. Let’s consider a more complex example: an application component with a custom implementation of DispatcherServlet for processing web requests.

This servlet loads its config from the demo-dispatcher-spring.xml file, so to see it working you should create an empty file with such name in the root source directory (e.g. web/src).

public class WebDispatcherServlet extends DispatcherServlet {
    private volatile boolean initialized = false;

    @Override
    public String getContextConfigLocation() {
        String configFile = "demo-dispatcher-spring.xml";
        File baseDir = new File(AppContext.getProperty("cuba.confDir"));

        String[] tokenArray = new StrTokenizer(configFile).getTokenArray();
        StringBuilder locations = new StringBuilder();

        for (String token : tokenArray) {
            String location;
            if (ResourceUtils.isUrl(token)) {
                location = token;
            } else {
                if (token.startsWith("/"))
                    token = token.substring(1);
                File file = new File(baseDir, token);
                if (file.exists()) {
                    location = file.toURI().toString();
                } else {
                    location = "classpath:" + token;
                }
            }
            locations.append(location).append(" ");
        }
        return locations.toString();
    }

    @Override
    protected WebApplicationContext initWebApplicationContext() {
        WebApplicationContext wac = findWebApplicationContext();
        if (wac == null) {
            ApplicationContext parent = AppContext.getApplicationContext();
            wac = createWebApplicationContext(parent);
        }

        onRefresh(wac);

        String attrName = getServletContextAttributeName();
        getServletContext().setAttribute(attrName, wac);
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Published WebApplicationContext of servlet '" + getServletName() +
                    "' as ServletContext attribute with name [" + attrName + "]");
        }

        return wac;
    }

    @Override
    public void init(ServletConfig config) throws ServletException {
        if (!initialized) {
            super.init(config);
            initialized = true;
        }
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        _service(response);
    }

    @Override
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        _service(res);
    }

    private void _service(ServletResponse res) throws IOException {
        String testMessage = AppContext.getApplicationContext().getBean(Messages.class).getMainMessage("testMessage");

        res.getWriter()
                .write("WebDispatcherServlet test message: " + testMessage);
    }
}

To register DispatcherServlet, you have to load the class manually, instantiate it and initialize, otherwise different ClassLoaders may cause an issue in case of SingleWAR/SingleUberJAR deployment. Moreover, the custom DispatcherServlet should be ready to double initialization - first time we initialize it manually, second time it is initialized by a servlet container.

Here is an example of a component that initializes WebDispatcherServlet:

@Component
public class WebInitializer {

    private static final String WEB_DISPATCHER_CLASS = "com.demo.comp.web.WebDispatcherServlet";
    private static final String WEB_DISPATCHER_NAME = "web_dispatcher_servlet";
    private final Logger log = LoggerFactory.getLogger(WebInitializer.class);

    @Inject
    private ServletRegistrationManager servletRegistrationManager;

    @EventListener
    public void initialize(ServletContextInitializedEvent e) {
        Servlet webDispatcherServlet = servletRegistrationManager.createServlet(e.getApplicationContext(), WEB_DISPATCHER_CLASS);
        ServletContext servletContext = e.getSource();
        try {
            webDispatcherServlet.init(new AbstractWebAppContextLoader.CubaServletConfig(WEB_DISPATCHER_NAME, servletContext));
        } catch (ServletException ex) {
            throw new RuntimeException("Failed to init WebDispatcherServlet");
        }
        servletContext.addServlet(WEB_DISPATCHER_NAME, webDispatcherServlet)
                .addMapping("/webd/*");
    }
}

The createServlet() method of the injected ServletRegistrationManager bean takes the application context from ServletContextInitializedEvent and the fully-qualified name of the WebDispatcherServlet class. In order to initialize the servlet, we pass the instance of ServletContext obtained from ServletContextInitializedEvent and the servlet name. The addMapping() method is used to define an HTTP mapping for accessing the servlet via URL: /webd/.

4.6. Logging

The platform uses Logback framework for logging.

To output to the log, use SLF4J API: get a logger for the current class and invoke one of its methods, for example:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Foo {
    // create logger
    private Logger log = LoggerFactory.getLogger(Foo.class);

    private void someMethod() {
        // output message with DEBUG level
        log.debug("someMethod invoked");
    }
}

Logs for the Middleware, Web Client and Web Portal blocks are configured at the application server level; in fast deployment mode the server is Tomcat.

4.6.1. Setting up Logging in Tomcat

This section covers the setup of logging in development environment.

Running Gradle setupTomcat task installs the Tomcat server into the project directory and performs its additional configuration. Particularly, setenv.bat and setenv.sh files are created in the tomcat/bin subfolder, and logback.xml is created in the tomcat/conf subfolder.

Among other things, the setenv.* files define loading parameters for the logback.xml configuration file using the CATALINA_OPTS variable.

logback.xml defines logging configuration. The file has the following structure:

  • appender elements define the "output device" for the log. The main appenders are FILE and CONSOLE. The level parameter of ThresholdFilter defines the message threshold. By default, it is DEBUG for a file and INFO for console. It means that ERROR, WARN, INFO and DEBUG messages are written to a file, while ERROR, WARN and INFO are written to console.

    The path to the log file for the file appender is defined in the file parameter. The default is tomcat/logs/app.log.

  • logger elements define the logger parameters that are used to print messages from the program code. Logger names are hierarchical, i.e. the settings of the com.company.sample logger have effect on the com.company.sample.core.CustomerServiceBean and com.company.sample.web.CustomerBrowse loggers, if the loggers do not explicitly override the settings with their own.

    Minimum logging level is defined by the level attribute. For example, if the category is INFO, then DEBUG and TRACE messages will not be logged. It should be kept in mind that message logging is also affected by the level threshold set in the appender.

You can quickly change logger levels and appender thresholds for a running server using the Administration > Server Log screen available in the web client. Any changes to the logging settings are effective only during server runtime and are not saved to a file. The screen also enables viewing and loading log files from the server logs folder (tomcat/logs).

The platform automatically adds the following information to the messages written to the file log:

  • application – the name of the web application that has logged the message. This information enables identifying messages from different application blocks (Middleware, Web Client), since they are written into the same file.

  • user – the login name of the user who invoked the code logging the message. This helps to track activity of a certain user in the log. If the code that logged a message was not invoked within a specific user session, the user information is not added.

For example, the following message has been written to the log by the code of the Middleware block (app-core), running under the admin user:

16:12:20.498 DEBUG [http-nio-8080-exec-7/app-core/admin] com.haulmont.cuba.core.app.DataManagerBean - loadList: ...

4.6.2. Useful Loggers

Below is a list of framework loggers that can be useful for application troubleshooting.

eclipselink.sql

If set to DEBUG, the EclipseLink ORM framework logs all its SQL statements together with their execution time. The logger is already defined in the standard logback.xml, so you have to only change its level. For example:

<configuration>
    ...
    <logger name="eclipselink.sql" level="DEBUG"/>

Example of a log output:

2018-09-21 12:48:18.583 DEBUG [http-nio-8080-exec-5/app-core/admin] com.haulmont.cuba.core.app.RdbmsStore - loadList: metaClass=sec$User, view=com.haulmont.cuba.security.entity.User/user.browse, query=select u from sec$User u, max=50
2018-09-21 12:48:18.586 DEBUG [http-nio-8080-exec-5/app-core/admin] eclipselink.sql - <t 891235430, conn 1084868057> SELECT t1.ID AS a1, t1.ACTIVE AS a2, t1.CHANGE_PASSWORD_AT_LOGON AS a3, t1.CREATE_TS AS a4, t1.CREATED_BY AS a5, t1.DELETE_TS AS a6, t1.DELETED_BY AS a7, t1.EMAIL AS a8, t1.FIRST_NAME AS a9, t1.IP_MASK AS a10, t1.LANGUAGE_ AS a11, t1.LAST_NAME AS a12, t1.LOGIN AS a13, t1.LOGIN_LC AS a14, t1.MIDDLE_NAME AS a15, t1.NAME AS a16, t1.PASSWORD AS a17, t1.POSITION_ AS a18, t1.TIME_ZONE AS a19, t1.TIME_ZONE_AUTO AS a20, t1.UPDATE_TS AS a21, t1.UPDATED_BY AS a22, t1.VERSION AS a23, t1.GROUP_ID AS a24, t0.ID AS a25, t0.DELETE_TS AS a26, t0.DELETED_BY AS a27, t0.NAME AS a28, t0.VERSION AS a29 FROM SEC_USER t1 LEFT OUTER JOIN SEC_GROUP t0 ON (t0.ID = t1.GROUP_ID) WHERE (t1.DELETE_TS IS NULL) LIMIT ? OFFSET ?
        bind => [50, 0]
2018-09-21 12:48:18.587 DEBUG [http-nio-8080-exec-5/app-core/admin] eclipselink.sql - <t 891235430, conn 1084868057> [1 ms] spent
com.haulmont.cuba.core.sys.AbstractWebAppContextLoader

If set to TRACE, the framework logs application properties defined in files and coming from application components when the server starts, which can help in case of startup problems.

Note that you should also set an appropriate appender to TRACE, because usually appenders are set to a higher threshold. For example:

<configuration>
    ...
    <appender name="File" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>TRACE</level>
        </filter>
    ...
    <logger name="com.haulmont.cuba.core.sys.AbstractWebAppContextLoader" level="TRACE"/>

Example of a log output:

2018-09-21 12:38:59.525 TRACE [localhost-startStop-1] com.haulmont.cuba.core.sys.AbstractWebAppContextLoader - AppProperties of the 'core' block:
cuba.anonymousSessionId=9c91dbdf-3e73-428e-9088-d586da2434c5
cuba.automaticDatabaseUpdate=true
...

4.7. Debugging

This section explains how to use step-by-step debugging in CUBA applications.

4.7.1. Connecting a Debugger

You can start Tomcat server in debug mode by either running the Gradle task

gradlew start

or by running the bin/debug.* command file of the installed Tomcat.

After this, the server will accept debugger connections over port 8787. Port number can be changed in the bin/setenv.* file, in the JPDA_OPTS variable.

For debugging in Intellij IDEA you need to create a new Remote type Run/Debug Configuration element in the application project and set its Port property to 8787.

4.7.2. Debug Version of Widgetset

The easiest way to debug the application on the client side without GWT Super Dev Mode is to use the debug configuration inside the web module configuration.

  1. Add the new debug configuration inside webModule:

    configure(webModule) {
        configurations {
            webcontent
            debug // a new configuration
        }
        ''''''
    }
  2. Add the debug dependency inside the dependencies block of webModule:

    dependencies {
        provided(servletApi)
        compile(guiModule)
        debug("com.haulmont.cuba:cuba-web-toolkit:$cubaVersion:debug@zip")
    }

    If the charts add-on is added, then debug("com.haulmont.charts:charts-web-toolkit:$cubaVersion:debug@zip") must be used.

  3. Add deploy.doLast task to the webModule configure block:

    task deploy.doLast {
        project.delete "$cuba.tomcat.dir/webapps/app/VAADIN/widgetsets"
    
        project.copy {
            from zipTree(configurations.debug.singleFile)
            into "$cuba.tomcat.dir/webapps/app"
        }
    }

The debug scenarios will be deployed in the $cuba.tomcat.dir/webapps/app/VAADIN/widgetsets/com.haulmont.cuba.web.toolkit.ui.WidgetSet directory of the project.

4.7.3. Debugging Web Widgets

You can use GWT Super Dev Mode to debug web widgets on the browser side.

  1. Setup the debugWidgetSet task in build.gradle.

  2. Deploy the application and start Tomcat.

  3. Run the debugWidgetSet task:

    gradlew debugWidgetSet

    The running GWT Code Server will recompile your widgetset on modification.

  4. Open http://localhost:8080/app?debug&superdevmode in Chrome web browser and wait for the widgetset is built for the first time.

  5. Open the debug console in Chrome:

    debugWidgetSet chrome console
  6. After changing the Java code in the web-toolkit module, refresh the web page in the browser. The widgetset will be rebuilt incrementally in approximately 8-10 seconds.

4.8. Testing

CUBA applications can be tested using well-known approaches: unit, integration and UI testing.

Unit tests are well suited for testing business logic encapsulated in specific classes and loosely coupled with the application infrastructure. You can just create the test directory in the global, core or web module of your project and start writing JUnit tests. If you need mocks, add a dependency on your favorite mocking framework or JMockit which is already used by CUBA. The mocking framework dependency must be added to the build.gradle file before JUnit:

configure([globalModule, coreModule, webModule]) {
    apply(plugin: 'java')
    apply(plugin: 'maven')
    apply(plugin: 'cuba')

    dependencies {
        testCompile('org.jmockit:jmockit:1.39') // add mocking framework here
        testCompile('junit:junit:4.12')
    }

    // ...

Integration tests run in the Spring container, so they are able to test most aspects of your application, including interaction with the database and UI screens. This section describes how to create integration tests on the middleware and web tiers.

For UI tests, we recommend using the Masquerade library which provides a set of useful abstractions for testing CUBA applications. See README and Wiki sections on GitHub.

4.8.1. Middleware Integration Tests

Middleware integration tests run in a fully functional Spring container connected to the database. In such tests, you can run code on all layers of the middleware, from services down to ORM.

In order to configure and start the middleware Spring container in tests, create a subclass of the com.haulmont.cuba.testsupport.TestContainer base class in your project and use its instance in tests as a JUnit Rule.

Below is an example of the container class and an integration test for the Sales project described in Quick Start. All classes must be located in the test directory of the core module.

package com.company.sales;

import com.haulmont.cuba.testsupport.TestContainer;

import java.util.ArrayList;
import java.util.Arrays;

public class SalesTestContainer extends TestContainer {

    public SalesTestContainer() {
        super();
        appComponents = new ArrayList<>(Arrays.asList(
                "com.haulmont.cuba"
                // add CUBA premium add-ons here
                // "com.haulmont.bpm",
                // "com.haulmont.charts",
                // "com.haulmont.fts",
                // "com.haulmont.reports",
                // and custom app components if any
        ));
        appPropertiesFiles = Arrays.asList(
                // List the files defined in your web.xml
                // in appPropertiesConfig context parameter of the core module
                "com/company/sales/app.properties",
                // Add this file which is located in CUBA and defines some properties
                // specifically for test environment. You can replace it with your own
                // or add another one in the end.
                "com/haulmont/cuba/testsupport/test-app.properties");
        initDbProperties();
    }

    private void initDbProperties() {
        dbDriver = "org.postgresql.Driver";
        dbUrl = "jdbc:postgresql://localhost/sales_test";
        dbUser = "cuba";
        dbPassword = "cuba";
    }

    public static class Common extends SalesTestContainer {

        // A common singleton instance of the test container which is initialized once for all tests
        public static final SalesTestContainer.Common INSTANCE = new SalesTestContainer.Common();

        private static volatile boolean initialized;

        private Common() {
        }

        @Override
        public void before() throws Throwable {
            if (!initialized) {
                super.before();
                initialized = true;
            }
            setupContext();
        }

        @Override
        public void after() {
            cleanupContext();
            // never stops - do not call super
        }
    }
}

An example of the custom test-app.properties file:

cuba.webContextName = app-core
sales.someProperty = someValue

We recommend using a separate test database, which can be created, for example, by the following Gradle task defined in build.gradle:

configure(coreModule) {
...
    task createTestDb(dependsOn: assemble, description: 'Creates local Postgres database for tests', type: CubaDbCreation) {
        dbms = 'postgres'
        dbName = 'sales_test'
        dbUser = 'cuba'
        dbPassword = 'cuba'
    }

The test container should be used in test classes as a JUnit rule specified by the @ClassRule annotation:

package com.company.sales;

import com.company.sales.entity.Customer;
import com.haulmont.cuba.core.global.*;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

public class CustomerTest {

    // Using the common singleton instance of the test container which is initialized once for all tests
    @ClassRule
    public static SalesTestContainer cont = SalesTestContainer.Common.INSTANCE;

    private Metadata metadata;

    @Before
    public void setUp() throws Exception {
        metadata = cont.metadata();
    }

    @Test
    public void testCreateCustomer() throws Exception {
        // Get a managed bean (or service) from container
        DataManager dataManager = AppBeans.get(DataManager.class);

        // Create new Customer
        Customer customer = metadata.create(Customer.class);
        customer.setName("Test customer");

        // Save the customer to the database
        dataManager.commit(customer);

        // Load the customer by ID
        Customer loaded = dataManager.load(
                LoadContext.create(Customer.class).setId(customer.getId()).setView(View.LOCAL));

        assertNotNull(loaded);
        assertEquals(customer.getName(), loaded.getName());

        // Remove the customer
        dataManager.remove(loaded);
    }
}
Useful container methods

The TestContainer class contains the following methods that can be used in the test code (see the CustomerLoadTest example above):

  • persistence() – returns the reference to the Persistence interface.

  • metadata() – returns the reference to the Metadata interface.

  • deleteRecord() – this set of overloaded methods is aimed to be used in @After methods to clean up the database after tests.

Logging

The test container sets up logging according to the test-logback.xml file provided by the platform. It is contained in the root of the cuba-core-tests artifact.

If you want to configure logging levels for your tests, do the following:

  • Copy test-logback.xml from the platform artifact to the root of the test folder of your project’s core module, e.g. as my-test-logback.xml.

  • Configure appenders and loggers in my-test-logback.xml.

  • Add a static initializer to your test container to specify the location of your logback configuration file in the logback.configurationFile system property:

    public class MyTestContainer extends TestContainer {
    
        static {
            System.setProperty("logback.configurationFile", "my-test-logback.xml");
        }
    
        // ...
    }
Additional Data Stores

If your project uses additional data stores, you should create corresponding JDBC data sources in your test container. For example, if you have mydb datastore which is a PostgreSQL database, add the following method to the test container class:

public class MyTestContainer extends TestContainer {
    // ...

    @Override
    protected void initDataSources() {
        super.initDataSources();
        try {
            Class.forName("org.postgresql.Driver");
            TestDataSource mydbDataSource = new TestDataSource(
                    "jdbc:postgresql://localhost/mydatabase", "db_user", "db_password");
            TestContext.getInstance().bind(
                    AppContext.getProperty("cuba.dataSourceJndiName_mydb"), mydbDataSource);
        } catch (ClassNotFoundException | NamingException e) {
            throw new RuntimeException("Error initializing datasource", e);
        }
    }
}

Also, if the additional database type is different from the main one, you should add its driver as the testRuntime dependency to the core module in build.gradle, for example:

configure(coreModule) {
    // ...
    dependencies {
        // ...
        testRuntime(hsql)
        jdbc('org.postgresql:postgresql:9.4.1212')
        testRuntime('org.postgresql:postgresql:9.4.1212') // add this
    }

4.8.2. Web Integration Tests

Web integration tests run in the Spring container of the Web Client block. This test container works independently from the middleware because the framework automatically creates stubs for all middleware services. The testing infrastructure consists of the following classes located in the com.haulmont.cuba.web.testsupport package and nested packages:

  • TestContainer - a wrapper of the Spring container to be used as a base class for project-specific containers.

  • TestServiceProxy - provides default stubs for middleware services. This class can be used to register service mocks specific to a test, see its mock() static method.

  • DataServiceProxy - default stub for DataManager. It contains an implementation for commit() method which mimics the behavior of the real data store: makes new entities detached, increments versions, etc. The loading methods return null and empty collections.

  • TestUiEnvironment - provides a set of methods to configure and obtain TestContainer. Instance of this class should be used in tests as a JUnit Rule.

  • TestEntityFactory - convenient factory for creating entity instances for tests. The factory can be obtained from TestContainer.

Although the framework provides default stubs for services, you may want to create your own service mocks in tests. To create mocks, you can use any mocking framework by adding it as a dependency as explained in the section above. Service mocks are registered using the TestServiceProxy.mock() method.

Example of web integration test container

Create test directory in the web module. Then create project’s test container class in an appropriate package of the test directory:

package com.company.sales.web;

import com.haulmont.cuba.web.testsupport.TestContainer;

import java.util.ArrayList;
import java.util.Arrays;

public class SalesWebTestContainer extends TestContainer {

    public SalesWebTestContainer() {
        appComponents = new ArrayList<>(Arrays.asList(
                "com.haulmont.cuba"
                // add CUBA add-ons and custom app components here
        ));
        appPropertiesFiles = Arrays.asList(
                // List the files defined in your web.xml
                // in appPropertiesConfig context parameter of the web module
                "com/company/sales/web-app.properties",
                // Add this file which is located in CUBA and defines some properties
                // specifically for test environment. You can replace it with your own
                // or add another one in the end.
                "com/haulmont/cuba/web/testsupport/test-web-app.properties"
        );
    }

    public static class Common extends SalesWebTestContainer {

        // A common singleton instance of the test container which is initialized once for all tests
        public static final SalesWebTestContainer.Common INSTANCE = new SalesWebTestContainer.Common();

        private static volatile boolean initialized;

        private Common() {
        }

        @Override
        public void before() throws Throwable {
            if (!initialized) {
                super.before();
                initialized = true;
            }
            setupContext();
        }

        @Override
        public void after() {
            cleanupContext();
            // never stops - do not call super
        }
    }
}
Example of UI screen test

Below is an example of web integration test that checks the state of the edited entity after some user action.

package com.company.sales.web.customer;

import com.company.sales.entity.Customer;
import com.company.sales.web.SalesWebTestContainer;
import com.haulmont.cuba.gui.Screens;
import com.haulmont.cuba.gui.components.Button;
import com.haulmont.cuba.gui.screen.OpenMode;
import com.haulmont.cuba.web.app.main.MainScreen;
import com.haulmont.cuba.web.testsupport.TestEntityFactory;
import com.haulmont.cuba.web.testsupport.TestEntityState;
import com.haulmont.cuba.web.testsupport.TestUiEnvironment;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

import java.util.Collections;

import static org.junit.Assert.*;

public class CustomerEditInteractionTest {

    @Rule
    public TestUiEnvironment environment =
            new TestUiEnvironment(SalesWebTestContainer.Common.INSTANCE).withUserLogin("admin"); (1)

    private Customer customer;

    @Before
    public void setUp() throws Exception {
        TestEntityFactory<Customer> customersFactory =
                environment.getContainer().getEntityFactory(Customer.class, TestEntityState.NEW);

        customer = customersFactory.create(Collections.emptyMap()); (2)
    }

    @Test
    public void testGenerateName() {
        Screens screens = environment.getScreens(); (3)

        screens.create(MainScreen.class, OpenMode.ROOT).show(); (4)

        CustomerEdit customerEdit = screens.create(CustomerEdit.class); (5)
        customerEdit.setEntityToEdit(customer);
        customerEdit.show();

        assertNull(customerEdit.getEditedEntity().getName());

        Button generateBtn = (Button) customerEdit.getWindow().getComponent("generateBtn"); (6)
        customerEdit.onGenerateBtnClick(new Button.ClickEvent(generateBtn)); (7)

        assertEquals("Generated name", customerEdit.getEditedEntity().getName());
    }
}
1 - define test environment with the shared container and admin user in the user session stub.
2 - create entity instance in the new state.
3 - get Screens infrastructure object from the environment.
4 - open main screen, which is required for opening application screens.
5 - create, initialize and open an entity editor screen.
6 - get Button component.
7 - create a click event and invoke the controller’s method that reacts on the click.
Example of test for loading data in a screen

Below is an example of web integration test that checks the correctness of loaded data.

package com.company.sales.web.customer;

import com.company.sales.entity.Customer;
import com.company.sales.web.SalesWebTestContainer;
import com.haulmont.cuba.core.app.DataService;
import com.haulmont.cuba.core.entity.Entity;
import com.haulmont.cuba.core.global.LoadContext;
import com.haulmont.cuba.gui.Screens;
import com.haulmont.cuba.gui.model.InstanceContainer;
import com.haulmont.cuba.gui.screen.OpenMode;
import com.haulmont.cuba.gui.screen.UiControllerUtils;
import com.haulmont.cuba.web.app.main.MainScreen;
import com.haulmont.cuba.web.testsupport.TestEntityFactory;
import com.haulmont.cuba.web.testsupport.TestEntityState;
import com.haulmont.cuba.web.testsupport.TestUiEnvironment;
import com.haulmont.cuba.web.testsupport.proxy.TestServiceProxy;
import mockit.Delegate;
import mockit.Expectations;
import mockit.Mocked;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

import static org.junit.Assert.assertEquals;

public class CustomerEditLoadDataTest {

    @Rule
    public TestUiEnvironment environment =
            new TestUiEnvironment(SalesWebTestContainer.Common.INSTANCE).withUserLogin("admin");

    @Mocked
    private DataService dataService; (1)

    private Customer customer;

    @Before
    public void setUp() throws Exception {
        new Expectations() {{ (2)
            dataService.load((LoadContext<? extends Entity>) any);
            result = new Delegate() {
                Entity load(LoadContext lc) {
                    if ("sales_Customer".equals(lc.getEntityMetaClass())) {
                        return customer;
                    } else
                        return null;
                }
            };
        }};

        TestServiceProxy.mock(DataService.class, dataService); (3)

        TestEntityFactory<Customer> customersFactory =
                environment.getContainer().getEntityFactory(Customer.class, TestEntityState.DETACHED);

        customer = customersFactory.create(
                "name", "Homer", "email", "homer@simpson.com"); (4)
    }

    @After
    public void tearDown() throws Exception {
        TestServiceProxy.clear(); (5)
    }

    @Test
    public void testLoadData() {
        Screens screens = environment.getScreens();

        screens.create(MainScreen.class, OpenMode.ROOT).show();

        CustomerEdit customerEdit = screens.create(CustomerEdit.class);
        customerEdit.setEntityToEdit(customer);
        customerEdit.show();

        InstanceContainer customerDc = UiControllerUtils.getScreenData(customerEdit).getContainer("customerDc"); (6)
        assertEquals(customer, customerDc.getItem());
    }
}
1 - define data service mock using JMockit framework.
2 - define the mock behavior.
3 - register the mock.
4 - create entity instance in the detached state.
5 - remove the mock after the test completes.
6 - get data container.

4.9. Hot Deploy

CUBA Platform supports Hot Deploy technology which helps to apply project changes to the running application immediately without the need to restart the application server. In essence, hot deployment is performed by copying updated resources and Java source files of the project to the configuration directory of the application, and then the running application compiles the source code and loads new classes and resources.

How it works

When you make some changes in the source code, Studio copies the changed files to the configuration directory of the web application (tomcat/conf/app or tomcat/conf/app-core). The resources in the configuration directory have priority over the resources in the JAR files of the application, so the running application will load these resources next time it needs them. If it encounters Java source files, it compiles them on the fly and loads the resulting classes.

Studio also sends signals to the application to clear appropriate caches in order to make it load the changed resources. These are messages cache and the configurations of views, registered screens and menu.

When the application server is restarted, all files in the configuration directory are removed, and the JAR files contain the latest versions of your code.

What can be hot deployed

Other UI and middleware classes and beans, including their static methods, are hot deployed only if some screen file or a middleware service implementation that uses them has also been changed.

The reason for this is that class reloading is started by a signal: for screen controllers it is the screen reopening by a user, for services - Studio generates a special trigger file that is recognized by the server and is used to reload the particular service class and all its dependencies.

What cannot be hot deployed
Usage of hot deploy in Studio

Hot deploy settings can be configured in Studio: click CUBA > Settings in the main menu and select CUBA > Project settings element.

  • Click Hot Deploy Settings link to configure mappings between source paths and Tomcat directories.

  • The Instant hot deploy checkbox allows you to turn off automatic hot deploy for the current project.

When the instant hot deploy is disabled, you can manually trigger it by clicking CUBA > Build Tasks > Hot Deploy To Configuration Directory in the main menu.

5. Application Deployment

This chapter describes different aspects of CUBA applications deployment and operation.

Below is a diagram showing a possible deployment structure. It eliminates a single point of failure, provides load balancing and connection of different clients.

DeploymentStructure

In the simplest case, however, an application can be installed on a single computer that contains also the database. Various deployment options depending on load and fault tolerance requirements are described in detail in Application Scaling.

5.1. Application Home

Application home is a file system directory where Application Directories described below can be placed together. It is used in all deployment scenarios except Fast Deployment in Tomcat. In the latter case, the application directories are located in specific Tomcat folders.

The application home is formed simply by specifying a common root for the application directories. It is normally done in the /WEB-INF/local.app.properties file inside of a WAR or UberJAR file.

  • If you build a WAR file, you have to define the path to the application home in the buildWar Gradle task. You can specify an absolute path or a path relative to the server working directory if you know in advance where the WAR will be deployed. If not, you can specify a placeholder for a Java system property and provide the real path at runtime.

    Example of an application home set at runtime:

    • Task configuration:

      task buildWar(type: CubaWarBuilding) {
          appHome = '${app.home}'
          // ...
      }
    • Content of /WEB-INF/local.app.properties after building the WAR:

      cuba.logDir = ${app.home}/logs
      cuba.confDir = ${app.home}/${cuba.webContextName}/conf
      cuba.tempDir = ${app.home}/${cuba.webContextName}/temp
      cuba.dataDir = ${app.home}/${cuba.webContextName}/work
      ...
    • Command line providing app.home system property:

      java -Dapp.home=/opt/app_home ...

      The way to set the Java system property depends on your application server. For Tomcat, it is recommended to set it in the bin/setenv.sh (or bin/setenv.bat) file.

    • Resulting directory structure:

      /opt/app_home/
        app/
          conf/
          temp/
          work/
        app-core/
          conf/
          temp/
          work/
        logs/
  • In case of UberJAR, the home is set to the working directory by default, but can be redefined by the app.home Java system property. So in order to have the home in the same directory as described above for WAR example, it is enough to specify it in the command line as follows:

    java -Dapp.home=/opt/app_home -jar app.jar

5.2. Application Directories

This section describes file system directories used by various application blocks at runtime.

5.2.1. Configuration Directory

The configuration directory can contain resources that complement and override configuration, user interface, and business logic after the application is deployed. Overriding is provided by the loading mechanism of the Resources infrastructure interface. Firstly it performs the search in the configuration directory and then in the classpath, so that resources from the configuration directory take precedence over identically named resources located in JAR files and class directories.

The configuration directory may contain resources of the following types:

The location of the configuration directory is determined by the cuba.confDir application property. In case of fast deployment in Tomcat, it is a subdirectory with the web application name in the tomcat/conf directory, for example, tomcat/conf/app-core for the Middleware. For other deployment scenarios, the configuration directory is located inside the application home.

5.2.2. Work Directory

The application uses the work directory to store some persistent data and configuration.

For example, the file storage mechanism by default uses the filestorage subdirectory of the work directory. Besides, the Middleware block writes generated persistence.xml and orm.xml files into the work directory on startup.

Work directory location is determined by the cuba.dataDir application property. In case of fast deployment in Tomcat, it is a subdirectory with the name of the web application in the tomcat/work directory. For other deployment scenarios, the work directory is located inside the application home.

5.2.3. Log Directory

The content of log files is determined by the configuration of the Logback framework. The platform provides a default configuration file logback.xml in the classpath root. According to its settings, the log messages will be printed to the standard output.

In order to specify your own logging configuration, provide the logback.configurationFile Java system property with the path to your configuration file. See Setting up Logging in Tomcat for how to do it in case of fast deployment.

The logging configuration determines where the log file is located. It can be a directory inside a specific Tomcat folder (tomcat/logs in case of fast deployment), or a directory inside the application home. You can control it if you take the logback.xml from the deploy/tomcat/conf folder of your project and modify the logDir property, for example:

<configuration debug="false">
    <property name="logDir" value="${app.home}/logs"/>
    <!-- ... -->

The application should know where you store log files in order to allow administrators to view and load them in the Administration > Server Log screen. Use the cuba.logDir application property to set the location to the same directory as defined by logback.xml.

See also Logging.

5.2.4. Temporary Directory

This directory can be used by the application for creating arbitrary temporary files at runtime. The path to the temporary directory is determined by the cuba.tempDir application property. In case of fast deployment in Tomcat, it is a subdirectory with the name of the web application in the tomcat/temp directory. For other deployment scenarios, the temporary directory is located inside the application home.

5.2.5. Database Scripts Directory

This directory contains the set of SQL scripts to create and update the database. It is specific to the Middleware block.

The script directory structure reproduces the one described in Scripts to Create and Update Database, but it also has an additional top level that separates application components and the application scripts. The numbering of top-level directories is performed by project build tasks.

The DB scripts directory location is determined by cuba.dbDir application property. For fast deployment in Tomcat, it is the WEB-INF/db subdirectory of the middleware web application directory: tomcat/webapps/app-core/WEB-INF/db. For other deployment scenarios, the database scripts are located in the /WEB-INF/db directory inside WAR or UberJAR files.

5.3. Deployment Options

This section describes different ways to deploy CUBA applications.

5.3.1. Fast Deployment in Tomcat

Fast deployment is used by default when developing an application, as it provides minimum time for building, installation and starting the application. This option can also be used in production.

Fast deployment is performed using the deploy task that is declared for core and web modules in the build.gradle file. Before the first execution of deploy, a local Tomcat server should be set up and initialized using the setupTomcat task.

Please make sure your environment does not contain CATALINA_HOME, CATALINA_BASE and CLASSPATH variables. They may cause problems starting Tomcat without any reflection in logs. Reboot your computer after removing the variables.

As result of fast deployment, the following structure is created in the directory that is specified by the cuba.tomcat.dir property of the build.gradle script (only important directories and files are listed below):

bin/
    setenv.bat, setenv.sh
    startup.bat, startup.sh
    debug.bat, debug.sh
    shutdown.bat, shutdown.sh

conf/
    catalina.properties
    server.xml
    logback.xml
    logging.properties
    Catalina/
        localhost/
    app/
    app-core/

lib/
    hsqldb-2.2.9.jar

logs/
    app.log

shared/
    lib/

temp/
    app/
    app-core/

webapps/
    app/
    app-core/

work/
    app/
    app-core/
  • bin – the directory that contains tools to start and stop the Tomcat server:

    • setenv.bat, setenv.sh – the scripts that set environment variables. These scripts should be used for setting JVM memory parameters, specifying a configuration file for logging, configuring access to JMX, parameters to connect the debugger.

      If you experience slow startup of Tomcat on Linux installed in a virtual machine (VPS), try to configure a non-blocking entropy source for JVM in setenv.sh:

      CATALINA_OPTS="$CATALINA_OPTS -Djava.security.egd=file:/dev/./urandom"
    • startup.bat, startup.sh – the scripts that start Tomcat. The server starts in a separate console window on Windows and in the background on *nix.

      To start the server in the current console window, use the following commands instead of startup.*:

      > catalina.bat run

      $ ./catalina.sh run

    • debug.bat, debug.sh – the scripts that are similar to startup.*, but start Tomcat with an ability to connect the debugger. These scripts are launched when running the start the task of the build script.

    • shutdown.bat, shutdown.sh – the scripts that stop Tomcat.

  • conf – the directory that contains configuration files of Tomcat and its deployed applications.

    • catalina.properties – the Tomcat properties. To load shared libraries from the shared/lib directory (see below), this file should contain the following line:

      shared.loader=${catalina.home}/shared/lib/*.jar
    • server.xml – Tomcat configuration descriptor.

    • logback.xml – application logging configuration descriptor.

    • logging.properties – Tomcat server logging configuration descriptor.

    • Catalina/localhost – in this directory, context.xml application deployment descriptors can be placed. Descriptors located in this directory take precedence over the descriptors in the META-INF directories of the application. This approach is often convenient for the production environment. For example, with this descriptor, it is possible to specify the database connection parameters that are different from those specified in the application itself.

      Server-specific deployment descriptor should have the application name and the .xml extension. So, to create this descriptor, for example, for the app-core application, copy the contents of the webapps/app-core/META-INF/context.xml file to the conf/Catalina/localhost/app-core.xml file.

    • app – web client application configuration directory.

    • app-core – middleware application configuration directory.

  • lib – directory of the libraries that are loaded by the server’s common classloader. These libraries are available for both the server and all web applications deployed in it. In particular, this directory should have JDBC drivers of the utilized databases (hsqldb-XYZ.jar, postgresql-XYZ.jar, etc.)

  • logs – application and server logs directory. The main log file of the application is app.log (see Setting up Logging in Tomcat).

  • shared/lib – directory of libraries that are available to all deployed applications. These libraries classes are loaded by the server’s special shared classloader. Its usage is configured in the conf/catalina.properties file as described above.

    The deploy task of the build script copies all libraries not listed in the jarNames parameter, i.e. not specific for the given application, into this directory.

  • temp/app, temp/app-core – web client and the middleware applications temporary directories.

  • webapps – web application directories. Each application is located in its own subdirectory in the exploded WAR format.

    The deploy task of the build script create application subdirectories with the names specified in the appName parameters and, among other things, copy the libraries listed in the jarNames parameter to the WEB-INF/lib subdirectory for each application.

  • work/app, work/app-core – web client and the middleware applications work directories.

5.3.1.1. Using Tomcat in Production

By default, the fast deployment procedure creates the app and app-core web applications running on port 8080 of the local Tomcat instance. It means that the web client is available at http://localhost:8080/app.

You can use this Tomcat instance in production just by copying the tomcat directory to the server. All you have to do is to set up the server host name in both conf/app/local.app.properties and conf/app-core/local.app.properties files (create the files if they do not exist):

cuba.webHostName = myserver
cuba.webAppUrl = http://myserver:8080/app

Besides, set up the connection to you production database. You can do it in the context.xml file of your web application (webapps/app-core/META-INF/context.xml), or copy this file to conf/Catalina/localhost/app-core.xml as described in the previous section to separate development and production settings.

You can create the production database from a development database backup, or set up the automatic creation and further updating of the database. See Creating and Updating Database in Production.

Optional Configuration
  1. If you want to change the Tomcat port or web context (the last part of the URL after /), use Studio:

    • Open the project in Studio.

    • Go to Project Properties.

    • To change the web context, edit the Modules prefix field.

    • To change the Tomcat port, edit the Tomcat ports > HTTP port field.

  2. If you want to use the root context (http://myserver:8080/), rename app (or whatever you set on the previous step) directories to ROOT

    tomcat/
        conf/
            ROOT/
                local.app.properties
            app-core/
                local.app.properties
        webapps/
            ROOT/
            app-core/

    and use / as the web context name in conf/ROOT/local.app.properties:

    cuba.webContextName = /

5.3.2. WAR deployment to Jetty

Below is an example of deployment of the WAR files to the Jetty web server.

We are going to use the following directory structure:

  • C:\work\jetty-home\ - Jetty distribution folder;

  • C:\work\jetty-base\ - Jetty configuration directory, used to store Jetty configuration files, additional libraries and web applications.

  • C:\work\app_home\ - CUBA application home directory.

    1. Use the CUBA project tree > Project > Deployment > WAR Settings dialog in Studio or just manually add the buildWar task to the end of build.gradle:

      task buildWar(type: CubaWarBuilding) {
          appHome = '${app.home}'
          appProperties = ['cuba.automaticDatabaseUpdate': 'true']
          singleWar = false
      }

      Please note that we are building two separate WAR files for Middleware and Web Client blocks here.

    2. Start build process by running buildWar from the command line (provided that you have created the Gradle wrapper beforehand):

      gradlew buildWar

      As a result, the app-core.war and app.war files will be created in the build\distributions\war project subdirectory.

    3. Create an application home directory, for example, c:\work\app_home.

    4. Copy the logback.xml file from the development Tomcat (deploy/tomcat/conf project sub-folder) to the application home and edit the logDir property in this file:

      <property name="logDir" value="${app.home}/logs"/>
    5. Download and install Jetty to a local directory, for example, c:\work\jetty-home. This example has been tested on jetty-distribution-9.4.22.v20191022.zip.

    6. Create the c:\work\jetty-base directory, open the command prompt in it and execute:

      java -jar c:\work\jetty-home\start.jar --add-to-start=http,jndi,deploy,plus,ext,resources
    7. Create the c:\work\jetty-base\app-jetty.xml file defining the database connection pool. Contents of the file for a PostgreSQL database should be based on the following template:

      <?xml version="1.0"?>
      <!DOCTYPE Configure PUBLIC "-" "http://www.eclipse.org/jetty/configure_9_0.dtd">
      <Configure id="wac" class="org.eclipse.jetty.webapp.WebAppContext">
          <New id="CubaDS" class="org.eclipse.jetty.plus.jndi.Resource">
              <Arg/>
              <Arg>jdbc/CubaDS</Arg>
              <Arg>
                  <New class="org.apache.commons.dbcp2.BasicDataSource">
                      <Set name="driverClassName">org.postgresql.Driver</Set>
                      <Set name="url">jdbc:postgresql://localhost/db_name</Set>
                      <Set name="username">username</Set>
                      <Set name="password">password</Set>
                      <Set name="maxIdle">2</Set>
                      <Set name="maxTotal">20</Set>
                      <Set name="maxWaitMillis">5000</Set>
                  </New>
              </Arg>
          </New>
      </Configure>

      The app-jetty.xml file for MS SQL databases should correspond to the following template:

      <?xml version="1.0"?>
      <!DOCTYPE Configure PUBLIC "-" "http://www.eclipse.org/jetty/configure_9_0.dtd">
      <Configure id="wac" class="org.eclipse.jetty.webapp.WebAppContext">
          <New id="CubaDS" class="org.eclipse.jetty.plus.jndi.Resource">
              <Arg/>
              <Arg>jdbc/CubaDS</Arg>
              <Arg>
                  <New class="org.apache.commons.dbcp2.BasicDataSource">
                      <Set name="driverClassName">com.microsoft.sqlserver.jdbc.SQLServerDriver</Set>
                      <Set name="url">jdbc:sqlserver://server_name;databaseName=db_name</Set>
                      <Set name="username">username</Set>
                      <Set name="password">password</Set>
                      <Set name="maxIdle">2</Set>
                      <Set name="maxTotal">20</Set>
                      <Set name="maxWaitMillis">5000</Set>
                  </New>
              </Arg>
          </New>
      </Configure>
    8. Download the following JARs required for the DB connection pool and put them to the c:\work\jetty-base\lib\ext folder. Two of these files can be found in the deploy\tomcat\shared\lib project sub-folder:

      commons-pool2-2.6.2.jar
      commons-dbcp2-2.7.0.jar
      commons-logging-1.2.jar
    9. Copy the start.ini file from the Jetty distribution to the c:\work\jetty-base folder. Add the following text to the beginning of the c:\work\jetty-base\start.ini file:

      --exec
      -Xdebug
      -agentlib:jdwp=transport=dt_socket,address=8787,server=y,suspend=n
      -Dapp.home=c:\work\app_home
      -Dlogback.configurationFile=c:\work\app_home\logback.xml
      app-jetty.xml
    10. Copy the JDBC driver for your database to the c:\work\jetty-base\lib\ext directory. You can take the driver file from the deploy\tomcat\lib project directory. In case of PostgreSQL database, it is postgresql-42.2.5.jar.

    11. Copy WAR files to the c:\work\jetty-base\webapps directory.

    12. Open the command prompt in the c:\work\jetty-base directory and run:

      java -jar c:\work\jetty-home\start.jar
    13. Open http://localhost:8080/app in your web browser.

5.3.3. WAR deployment to WildFly

The WAR files with CUBA application can be deployed to the WildFly application server. An example below demonstrates how to deploy a CUBA application using PostgreSQL to the WildFly 18.0.0 server on Windows.

  1. Edit build.gradle and specify dependency in the global module dependencies section:

    runtime 'org.reactivestreams:reactive-streams:1.0.1'
  2. Assemble and deploy the project to the default Tomcat server in order to get all necessary dependencies locally.

  3. Configure the application home directory for the application:

    • Create a folder that will be fully available for WildFly server’s process. For example: C:\Users\UserName\app_home.

    • Copy the logback.xml file from tomcat/conf to this folder and edit the logDir property:

    <property name="logDir" value="${app.home}/logs"/>
  4. Configure the WildFly server

    • Install WildFly to a local folder, for example, to C:\wildfly.

    • Edit the C:\wildfly\bin\standalone.conf.bat file and add the following line to the end of the file:

    set "JAVA_OPTS=%JAVA_OPTS% -Dapp.home=%USERPROFILE%/app_home -Dlogback.configurationFile=%USERPROFILE%/app_home/logback.xml"

    Here we define the app.home system property with the application home directory and configure the logging by setting the path to the logback.xml file. You can also use an absolute path instead of %USERPROFILE% variable.

    • Compare the Hibernate Validator versions in WildFly and CUBA application. If the platform uses a newer version, replace the C:\wildfly\modules\system\layers\base\org\hibernate\validator\main\hibernate-validator-x.y.z-sometext.jar with the newer file from tomcat\shared\lib, for example, hibernate-validator-6.0.16.Final.jar.

    • Update the JAR file version number in the \wildfly\modules\system\layers\base\org\hibernate\validator\main\module.xml file.

    • To register PostgreSQL driver in WildFly, copy the postgresql-42.2.5.jar from tomcat\lib to C:\wildfly\standalone\deployments.

    • Setup WildFly logger: open the \wildfly\standalone\configuration\standalone.xml file and add two lines to the <subsystem xmlns="urn:jboss:domain:logging:{version}" block:

      <subsystem xmlns="urn:jboss:domain:logging:8.0">
          <add-logging-api-dependencies value="false"/>
          <use-deployment-logging-config value="false"/>
          . . .
      </subsystem>
  5. Create JDBC Datasource

    • Start WildFly by running standalone.bat

    • Open the administration console on http://localhost:9990. The first time you log in, you will be asked to create a user and a password.

    • Open the Configuration - Subsystems - Datasources and Drivers - Datasources tab and create a new datasource for your application:

    Name: Cuba
    JNDI Name: java:/jdbc/CubaDS
    JDBC Driver: postgresql-42.2.5.jar
    Driver Module Name: org.postgresql
    Driver Class Name: org.postgresql.Driver
    Connection URL: your database URL
    Username: your database username
    Password: your database password

    The JDBC driver will be available on the list of detected drivers if you have copied postgresql-x.y.z.jar as described above.

    Check the connection by clicking the Test connection button.

    • Activate the datasource.

    • Alternatively, you can create JDBC Datasource by using the bin/jboss-cli.bat command line utility:

      [disconnected /] connect
      [standalone@localhost:9990 /] data-source add --name=Cuba --jndi-name="java:/jdbc/CubaDS" --driver-name=postgresql-42.2.5.jar --user-name=dblogin --password=dbpassword --connection-url="jdbc:postgresql://dbhost/dbname"
      [standalone@localhost:9990 /] quit
  6. Build the application

    • Open CUBA project tree > Project > Deployment > WAR Settings dialog in Studio.

    • Check Build WAR checkbox.

    • Set ${app.home} in the Application home directory field.

    • Save the settings.

    • Open build.gradle in IDE and add the doAfter property to the buildWar task. This property will copy the WildFly deployment descriptor:

      task buildWar(type: CubaWarBuilding) {
          appProperties = ['cuba.automaticDatabaseUpdate' : true]
          singleWar = false
          appHome = '${app.home}'
          doAfter = {
              copy {
                  from 'jboss-deployment-structure.xml'
                  into "${project.buildDir}/tmp/buildWar/core/war/META-INF/"
              }
              copy {
                  from 'jboss-deployment-structure.xml'
                  into "${project.buildDir}/tmp/buildWar/web/war/META-INF/"
              }
          }
      }

      For a singleWAR configuration the task will be different:

      task buildWar(type: CubaWarBuilding) {
          webXmlPath = 'modules/web/web/WEB-INF/single-war-web.xml'
          appProperties = ['cuba.automaticDatabaseUpdate' : true]
          appHome = '${app.home}'
          doAfter = {
              copy {
                  from 'jboss-deployment-structure.xml'
                  into "${project.buildDir}/tmp/buildWar/META-INF/"
              }
          }
      }

      If your project also contains a Polymer module, add the following configuration to your single-war-web.xml file:

      <servlet>
          <servlet-name>default</servlet-name>
          <init-param>
              <param-name>resolve-against-context-root</param-name>
              <param-value>true</param-value>
          </init-param>
      </servlet>
    • In the project root folder, create the jboss-deployment-structure.xml file and add the WildFly deployment descriptor to it:

    <?xml version="1.0" encoding="UTF-8"?>
    <jboss-deployment-structure xmlns="urn:jboss:deployment-structure:1.0">
        <deployment>
            <exclusions>
                <module name="org.apache.commons.logging" />
                <module name="org.apache.log4j" />
                <module name="org.jboss.logging" />
                <module name="org.jboss.logging.jul-to-slf4j-stub" />
                <module name="org.jboss.logmanager" />
                <module name="org.jboss.logmanager.log4j" />
                <module name="org.slf4j" />
                <module name="org.slf4j.impl" />
                <module name="org.slf4j.jcl-over-slf4j" />
            </exclusions>
        </deployment>
    </jboss-deployment-structure>
    • Run the buildWar task to create WAR files.

  7. Copy the files app-core.war and app.war from build\distributions\war to WildFly directory \wildfly\standalone\deployments.

  8. Restart the WildFLy server.

  9. Your application will become available on http://localhost:8080/app. The log files will be saved in the application home: C:\Users\UserName\app_home\logs.

5.3.4. WAR deployment to Tomcat Windows Service

  1. Use the CUBA project tree > Project > Deployment > WAR Settings dialog in Studio or just manually add the buildWar task to the end of build.gradle:

    task buildWar(type: CubaWarBuilding) {
        appHome = '${app.home}'
        singleWar = true
        includeContextXml = true
        includeJdbcDriver = true
        appProperties = ['cuba.automaticDatabaseUpdate': true]
    }

    If the target server parameters differ from what you have on the local Tomcat used for fast deployment, provide appropriate application properties. For example, if the target server runs on port 9999 and you build separate WARs, the task definition should be as follows:

    task buildWar(type: CubaWarBuilding) {
        appHome = '${app.home}'
        singleWar = false
        includeContextXml = true
        includeJdbcDriver = true
        appProperties = [
            'cuba.automaticDatabaseUpdate': true,
            'cuba.webPort': 9999,
            'cuba.connectionUrlList': 'http://localhost:9999/app-core'
        ]
    }

    You can also specify a separate context.xml file to setup the connection to the production database or provide that file later on the server:

    task buildWar(type: CubaWarBuilding) {
        appHome = '${app.home}'
        singleWar = true
        includeContextXml = true
        includeJdbcDriver = true
        appProperties = ['cuba.automaticDatabaseUpdate': true]
        coreContextXmlPath = 'modules/core/web/META-INF/war-context.xml'
    }
  2. Run the buildWar Gradle task. As a result, app.war file (or several files if you build separate WARs) will be generated in the build/distributions directory of your project.

    gradlew buildWar
  3. Create the application home directory, e.g. C:\app_home.

  4. Download and install the Tomcat 9 Windows Service Installer from the Apache Tomcat official site.

  5. Go to the bin directory of the installed server and run tomcat9w.exe with the administrative rights in order to set Tomcat service settings:

    1. Set Maximum memory pool to 1024MB on the Java tab.

    2. Instruct Tomcat to use UTF-8 encoding by adding -Dfile.encoding=UTF-8 to the Java Options field.

    3. Specify application home folder by adding the -Dapp.home=c:/app_home to the Java Options field.

      tomcat service settings
  6. If you want to provide production database connection properties with a local file on the server, you can create a file in the conf\Catalina\localhost subfolder of the Tomcat server. The name of the file depends on the WAR file name, e.g. app.xml for single WAR and app-core.xml if separate WAR files are deployed. Copy contents of the context.xml to this file.

  7. With the default configuration all application log messages are appended to the logs/tomcat9-stdout.log file. You have two options how to customize logging configuration of the application:

    • Create the logback configuration file in the project. Specify path to this file for the logbackConfigurationFile parameter of the buildWar task (manually or with the help of Studio WAR Settings dialog).

    • Create the logging configuration file on the production server.

      Copy the logback.xml file from the development Tomcat (deploy/tomcat/conf project sub-folder) to the application home directory and edit the logDir property in this file:

      <property name="logDir" value="${app.home}/logs"/>

      Add the following line to the Java Options field in the Tomcat 9 Windows Service settings window to specify path to the logging configuration file:

      -Dlogback.configurationFile=C:/app_home/logback.xml
  8. Copy the generated WAR file(s) to the webapps directory of the Tomcat server.

  9. Restart the Tomcat service.

  10. Open http://localhost:8080/app in your web browser.

5.3.5. WAR deployment to Tomcat Linux Service

The example below has been developed for and tested on Ubuntu 18.04, with tomcat9 and tomcat8 packages.

  1. Use the CUBA project tree > Project > Deployment > WAR Settings dialog in Studio or just manually add the buildWar task to the end of build.gradle. You can specify a separate war-context.xml project file to specify connection settings to the production database or provide that file later on the server:

    task buildWar(type: CubaWarBuilding) {
        appHome = '${app.home}'
        singleWar = true
        includeContextXml = true
        includeJdbcDriver = true
        appProperties = ['cuba.automaticDatabaseUpdate': true]
        webXmlPath = 'modules/web/web/WEB-INF/single-war-web.xml'
        coreContextXmlPath = 'modules/core/web/META-INF/war-context.xml'
    }

    If the target server parameters differ from what you have on the local Tomcat used for fast deployment, provide appropriate application properties. For example, if the target server runs on port 9999 and you build separate WARs, the task definition should be as follows:

    task buildWar(type: CubaWarBuilding) {
        appHome = '${app.home}'
        singleWar = false
        includeContextXml = true
        includeJdbcDriver = true
        appProperties = [
            'cuba.automaticDatabaseUpdate': true,
            'cuba.webPort': 9999,
            'cuba.connectionUrlList': 'http://localhost:9999/app-core'
        ]
    }
  2. Run the buildWar gradle task. As a result, app.war file (or several files if you build separate WARs) will be generated in the build/distributions directory of your project.

    gradlew buildWar
  3. Install Tomcat 9 package:

    sudo apt install tomcat9
  4. Copy the generated app.war file to the /var/lib/tomcat9/webapps directory of the server. You can also remove the /var/lib/tomcat9/webapps/ROOT sample webapp folder if it exists.

    Tomcat 9 service runs from tomcat user by default. The owner of webapps folder is tomcat as well.

  5. Create the application home directory, e.g. /opt/app_home and make the Tomcat server user (tomcat) to be the owner of this folder:

    sudo mkdir /opt/app_home
    sudo chown tomcat:tomcat /opt/app_home
  6. Tomcat 9 service (unlike earlier versions of the Tomcat Debian package) is sandboxed by systemd and has limited write access to the file system. You can read more about this in the /usr/share/doc/tomcat9/README.Debian file. It is necessary to modify systemd configuration to allow Tomcat service write access to the application home folder:

    1. Create the override.conf file in the /etc/systemd/system/tomcat9.service.d/ directory:

      sudo mkdir /etc/systemd/system/tomcat9.service.d/
      sudo nano /etc/systemd/system/tomcat9.service.d/override.conf
    2. The contents of the override.conf file are the following:

      [Service]
      ReadWritePaths=/opt/app_home/
    3. Reload systemd configuration by invoking:

      sudo systemctl daemon-reload
  7. Create configuration file /usr/share/tomcat9/bin/setenv.sh with the following text:

    CATALINA_OPTS="$CATALINA_OPTS -Xmx1024m"
    CATALINA_OPTS="$CATALINA_OPTS -Dapp.home=/opt/app_home"

    If you experience slow startup of Tomcat installed in a virtual machine (VPS), add an additional line to the setenv.sh file:

    CATALINA_OPTS="$CATALINA_OPTS -Djava.security.egd=file:/dev/./urandom"
  8. If you want to provide production database connection properties with a local file on the server, you can create a file in the /var/lib/tomcat9/conf/Catalina/localhost/ folder. The name of the file depends on the WAR file name, e.g. app.xml for single WAR and app-core.xml if separate WAR files are deployed. Copy contents of the context.xml to this file.

  9. With the default configuration all application log messages are appended to the /var/log/syslog system journal. You have two options how to customize logging configuration of the application:

    • Create the logback configuration file in the project. Specify path to this file for the logbackConfigurationFile parameter of the buildWar task (manually or with the help of Studio WAR Settings dialog).

    • Create the logging configuration file on the production server.

      Copy the logback.xml file from the development Tomcat (deploy/tomcat/conf project sub-folder) to the application home directory and edit the logDir property in this file:

      <property name="logDir" value="${app.home}/logs"/>

      Add the following line to the setenv.sh script to specify path to the logging configuration file:

      CATALINA_OPTS="$CATALINA_OPTS -Dlogback.configurationFile=/opt/app_home/logback.xml"
  10. Restart the Tomcat service:

    sudo systemctl restart tomcat9
  11. Open http://localhost:8080/app in your web browser.

Differences when using tomcat8 package

CUBA supports deployment to both Tomcat 9 and Tomcat 8.5 versions. Please note the following differences when deploying to Tomcat 8.5:

  • Tomcat 8.5 is provided by the tomcat8 package

  • User name is tomcat8

  • Tomcat base directory is /var/lib/tomcat8

  • Tomcat home directory is /usr/share/tomcat8

  • Tomcat service does not use systemd sandboxing, so no need to change systemd settings.

  • Standard output and stderr messages are appended to the /var/lib/tomcat8/logs/catalina.out file.

Troubleshooting LibreOffice reporting integration when using tomcat9 package

You may experience problems when deploying to the tomcat9 package and using LibreOffice integration with the Reporting add-on. Error may be diagnosed with this message:

2019-12-04 09:52:37.015 DEBUG [OOServer: ERR] com.haulmont.yarg.formatters.impl.doc.connector.OOServer - ERR: (process:10403): dconf-CRITICAL **: 09:52:37.014: unable to create directory '/.cache/dconf': Read-only file system.  dconf will not work properly.

This error is caused by the home directory of the tomcat user pointing to a non-writable location. It can be fixed by changing tomcat user home directory to the /var/lib/tomcat9/work value:

# bad value
echo ~tomcat
/

# fix
sudo systemctl stop tomcat9
sudo usermod -d /var/lib/tomcat9/work tomcat
sudo systemctl start tomcat9

5.3.6. UberJAR Deployment

This is the simplest way to run your CUBA application in a production environment. You need to build an all-in-one JAR file using the buildUberJar Gradle task (see also the Deployment > UberJAR settings page in Studio) and then you can run the application from the command line using the java executable:

java -jar app.jar

All parameters of the application are defined at the build time, but can be overridden when running (see below). The default port of the web application is 8080 and it is available at http://host:8080/app. If your project has Polymer UI, by default it will be available at http://host:8080/app-front.

If you build separate JAR files for Middleware and Web Client, you can run them in the same way:

java -jar app-core.jar

java -jar app.jar

The default port of the web client is 8080 and it will try to connect to the middleware running on localhost:8079. So after running the above commands in two separate terminal windows, you will be able to connect to the web client at http://localhost:8080/app.

You can change the parameters defined at the build time by providing application properties via Java system properties. Besides, ports, context names and paths to Jetty configuration files can be provided as command line arguments.

Command line arguments
  • port - defines the port on which the embedded HTTP server will run. For example:

    java -jar app.jar -port 9090

    Please note that if you build separate JARs and specify a port for the core block, you need to provide the cuba.connectionUrlList application property with the corresponding address to the client blocks, for example:

    java -jar app-core.jar -port 7070
    
    java -Dcuba.connectionUrlList=http://localhost:7070/app-core -jar app.jar
  • contextName - a web context name for this application block. For example, in order to access your web client at http://localhost:8080/sales, run the following command:

    java -jar app.jar -contextName sales
  • frontContextName - a web context name for the Polymer UI (makes sense for single, web or portal JARs).

  • portalContextName - a web context name for the portal module running in the single JAR.

  • jettyEnvPath - a path to the Jetty environment file which will override build time settings specified in the coreJettyEnvPath parameter. It can be an absolute path or a path relative to the working directory.

  • jettyConfPath - a path to the Jetty server configuration file which will override build time settings specified in the webJettyConfPath/coreJettyConfPath/portalJettyConfPath parameter. It can be an absolute path or a path relative to the working directory.

Application Home

By default, the application home is the working directory. It means that application directories will be created in the folder where you have run the application. It can be redefined by the app.home Java system property. So for example, in order to have the home in /opt/app_home, specify the following on the command line:

java -Dapp.home=/opt/app_home -jar app.jar
Logging

If you want to modify built-in logging settings, provide the logback.configurationFile Java system property with an URL to load your configuration file, for example:

java -Dlogback.configurationFile=file:./logback.xml -jar app.jar

Here it is assumed that the logback.xml file is located in the folder where you start the application from.

In order to set the log output directory correctly, make sure the logDir property in the logback.xml points to the logs subdirectory of the application home:

<configuration debug="false">
    <property name="logDir" value="${app.home}/logs"/>
    <!-- ... -->
Specifying deployment-time application properties

It is possible to set application properties in the deployment environment, specifying values which are not available at the time when the application is built (e.g. for security reasons). Examples below are presented for the project named "sample" with default module prefix "app".

1) Modify web.xml for all web applications of your project. Change 3rd line in the appPropertiesConfig servlet context parameter value. Specify path to the local.app.properties file relative to the application home directory in the following format:

"file:${app.home}/web-application-name/conf/local.app.properties"

For example, for the app-core module you need to modify modules/core/web/WEB-INF/web.xml file and set the following content to the appPropertiesConfig:

<!-- Application properties config files -->
<context-param>
    <param-name>appPropertiesConfig</param-name>
    <param-value>
        classpath:com/company/sample/app.properties
        /WEB-INF/local.app.properties
        "file:${app.home}/app-core/conf/local.app.properties"
    </param-value>
</context-param>

2) Create local.app.properties files in the application home folder and fill them with necessary values:

admin@server:/opt/app_home$ cat app/conf/local.app.properties
cuba.web.loginDialogDefaultUser=<disabled>
cuba.web.loginDialogDefaultPassword=<disabled>

admin@server:/opt/app_home$ cat app-core/conf/local.app.properties
cuba.userSessionExpirationTimeoutSec = 7200

3) Ensure that you specify correct application home directory when starting the application:

java -Dapp.home=/opt/app_home -jar app.jar

4) Check startup logs. You should see the following lines, one for every web application:

14:49:44.875 INFO  c.h.c.c.s.AbstractWebAppContextLoader   - Loading
app properties from file:/opt/app_home/app-core/conf/local.app.properties
...
14:49:49.339 INFO  c.h.c.c.s.AbstractWebAppContextLoader   - Loading
app properties from file:/opt/app_home/app/conf/local.app.properties
...
Stopping an application

You can gracefully stop the application in the following ways:

  • Pressing Ctrl+C in the terminal window where the application is running.

  • Executing kill <PID> on Unix-like systems.

  • Sending a stop key (i.e. a character sequence) on a port specified in the command line of the running application. There are the following command line arguments:

    • stopPort - a port to listen for a stop key or to send the key to.

    • stopKey - a stop key. If not specified, SHUTDOWN is used.

    • stop - to stop another process by sending the key.

For example:

# Start application 1 and listen to SHUTDOWN key on port 9090
java -jar app.jar -stopPort 9090

# Start application 2 and listen to MYKEY key on port 9090
java -jar app.jar -stopPort 9090 -stopKey MYKEY

# Shutdown application 1
java -jar app.jar -stop -stopPort 9090

# Shutdown application 2
java -jar app.jar -stop -stopPort 9090 -stopKey MYKEY
5.3.6.1. Configuring HTTPS for UberJAR

Below is an example of configuring HTTPS with a self-signed certificate for UberJAR deployment.

  1. Generate keys and certificates with in-built JDK tool Java Keytool:

    keytool -keystore keystore.jks -alias jetty -genkey -keyalg RSA
  2. Create the jetty.xml file with SSL configuration in the project root folder:

    <Configure id="Server" class="org.eclipse.jetty.server.Server">
        <Call name="addConnector">
            <Arg>
                <New class="org.eclipse.jetty.server.ServerConnector">
                    <Arg name="server">
                        <Ref refid="Server"/>
                    </Arg>
                    <Set name="port">8090</Set>
                </New>
            </Arg>
        </Call>
        <Call name="addConnector">
            <Arg>
                <New class="org.eclipse.jetty.server.ServerConnector">
                    <Arg name="server">
                        <Ref refid="Server"/>
                    </Arg>
                    <Arg>
                        <New class="org.eclipse.jetty.util.ssl.SslContextFactory">
                            <Set name="keyStorePath">keystore.jks</Set>
                            <Set name="keyStorePassword">password</Set>
                            <Set name="keyManagerPassword">password</Set>
                            <Set name="trustStorePath">keystore.jks</Set>
                            <Set name="trustStorePassword">password</Set>
                        </New>
                    </Arg>
                    <Set name="port">8443</Set>
                </New>
            </Arg>
        </Call>
    </Configure>

    The keyStorePassword, keyManagerPassword, and trustStorePassword should correspond to those set by Keytool.

  3. Add jetty.xml to the build task configuration:

    task buildUberJar(type: CubaUberJarBuilding) {
        singleJar = true
        coreJettyEnvPath = 'modules/core/web/META-INF/jetty-env.xml'
        appProperties = ['cuba.automaticDatabaseUpdate' : true]
        webJettyConfPath = 'jetty.xml'
    }
  4. Build Uber JAR as described in the UberJAR Deployment section.

  5. Put the keystore.jks in the same folder with JAR distribution of your project and start Uber JAR.

    The application will be available at https://localhost:8443/app.

5.3.7. Deployment with Docker

This section covers deployment of CUBA applications in Docker containers.

We will take the sample project developed in the Quick Start section, migrate it to PostgreSQL database and build UberJAR to run in a container. In fact, an application built as a WAR file would also work with a containerized Tomcat, but it would require a bit more configuration, so for the demo purposes we will stick with UberJAR.

Configure and build UberJAR

Clone the sample project from https://github.com/cuba-platform/sample-sales-cuba7 and open it in CUBA Studio.

First, change your database type to PostgreSQL:

  1. Click CUBA > Main Data Store Settings…​ in the main menu.

  2. Select PostgreSQL in the Database type field and click OK.

  3. Click CUBA > Generate Database Scripts in the main menu. Studio opens the Database Scripts dialog with generated scripts. Click Save and close in the dialog.

  4. Click CUBA > Create Database in the main menu. Studio creates the sales database on the local PostgreSQL server.

Next, configure a Gradle task for building UberJAR.

  1. Click CUBA > Deployment > UberJAR Settings main menu item.

  2. Select Build Uber JAR and Single Uber JAR checkboxes.

  3. Click Generate button next to the Logback configuration file field.

  4. Click Generate button next to the Custom Jetty environment file field. Make sure that PostgreSQL is selected and enter postgres instead of localhost in the Database URL field. This is needed for work with a containerized database described below.

  5. Click OK. Studio adds the buildUberJar task to the build.gradle file.

  6. To ensure the correct location of the log files, open the generated etc/uber-jar-logback.xml file and change the logDir property as follows:

    <property name="logDir" value="${app.home}/logs"/>

    Also, make sure the Logback configuration file limits the level of the org.eclipse.jetty logger at least to INFO. If there is no such logger in the file, add it:

    <logger name="org.eclipse.jetty" level="INFO"/>

Run the task to create the JAR file:

./gradlew buildUberJar
Create Docker image

Now let’s create Dockerfile and build a docker image with our application.

  1. Create the docker-image folder in the project.

  2. Copy the JAR file from build/distributions/uberJar into this folder.

  3. Create a Dockerfile with the following instructions:

    FROM openjdk:8
    
    COPY . /opt/sales
    
    CMD java -Dapp.home=/opt/sales-home -jar /opt/sales/app.jar

The app.home Java system property defines a directory for the application home where all logs and other files created by the application will be stored. When running the container, we will be able to map this directory to a host computer directory for easy access to logs and other data including files uploaded to FileStorage.

Now build the image:

  1. Open the terminal in the project root folder.

  2. Run the build command passing the image name in the -t option and the directory where Dockerfile is located:

    docker build -t sales docker-image

Check that the sales image is shown when you execute the docker images command.

Run application and database containers

The application is now ready to run in the container, but we also need a containerized PostgreSQL database. In order to manage two containers - one with the application and another with the database, we will use Docker Compose.

Create docker-compose.yml file in the project root with the following content:

version: '2'

services:
  postgres:
    image: postgres:9
    environment:
      - POSTGRES_DB=sales
      - POSTGRES_USER=cuba
      - POSTGRES_PASSWORD=cuba
    ports:
      - "5433:5432"
  web:
    image: sales
    volumes:
      - /Users/me/sales-home:/opt/sales-home
    ports:
      - "8080:8080"

Pay attention to the following parts of the file:

  • The volumes section maps the container’s /opt/sales-home path which is the application home directory to the host’s /Users/me/sales-home path. It means that the application logs will be available in the /Users/me/sales-home/logs directory of the host computer.

  • The PostgreSQL internal port 5432 is mapped to the host’s port 5433 to avoid possible conflicts with a PostgreSQL instance running on the host computer. Using this port, you can access the database from outside of the container, for example to backup it:

    pg_dump -Fc -h localhost -p 5433 -d sales -U cuba > /Users/me/sales.backup
  • The application container exposes port 8080, so the application UI will be available at http://localhost:8080/app on the host computer.

To start the application and the database, open the terminal in the directory of the docker-compose.yml file and run:

docker-compose up

5.3.8. Deployment to Jelastic Cloud

Below is an example of building and deployment application to the Jelastic cloud.

Please note that only projects using PostgreSQL or HSQL databases are currently supported.

  1. First, create a free test account in the Jelastic cloud using a web browser.

  2. Create a new environment where the application WAR will be deployed:

    • Click New Environment.

      jelasticEnvironment
    • Specify the settings in the window that appears: a compatible environment should have Java 8, Tomcat 8 and PostgreSQL 9.1+ (if the project uses PostgreSQL database). In the Environment Name field, specify a unique environment name and click Create.

    • If the created environment uses PostgreSQL, you will receive an email with the database connection details. Go to the database administration web interface using the link in the email received after the creation of the environment and create an empty database. The database name should be specified later in the custom context.xml.

  3. Build Single WAR file using CUBA Studio:

    • Select CUBA > Deployment > WAR Settings in the main menu.

    • Select Build WAR checkbox.

    • Enter .. in the Application home directory field.

    • Select Include JDBC driver and Include Tomcat’s context.xml checkboxes.

    • If your project uses PostgreSQL, press Generate button next to the Custom context.xml path field. Specify the database user, password, host, and name of the database that you created earlier in the Jelastic web interface.

      customContextXml
    • Select Single WAR for Middleware and Web Client checkbox.

    • Press Generate button next to the Custom web.xml path field. Studio will generate a special web.xml of the single WAR comprising the Middleware and Web Client application blocks.

      jelasticWarSettings
    • Add cuba.logDir property in the App properties field:

      appProperties = ['cuba.automaticDatabaseUpdate': true,
      'cuba.logDir': '${catalina.base}/logs']
    • Click the OK button. Studio adds the buildWar task to the build.gradle file.

      task buildWar(type: CubaWarBuilding) {
          includeJdbcDriver = true
          includeContextXml = true
          appProperties = ['cuba.automaticDatabaseUpdate': true,
                           'cuba.logDir'                 : '${catalina.base}/logs']
          webXmlPath = 'modules/web/web/WEB-INF/single-war-web.xml'
          appHome = '..'
          coreContextXmlPath = 'modules/core/web/META-INF/war-context.xml'
      }
    • If your project uses HSQLDB, open the buildWar task in the build.gradle and add the hsqlInProcess = true property in order to run the embedded HSQL server while deploying the WAR file. Make sure that the coreContextXmlPath property is not set.

      task buildWar(type: CubaWarBuilding) {
          appProperties = ['cuba.automaticDatabaseUpdate': true, 'cuba.logDir': '${catalina.base}/logs']
          appHome = '..'
          includeContextXml = true
          webXmlPath = 'modules/web/web/WEB-INF/single-war-web.xml'
          includeJdbcDriver = true
          hsqlInProcess = true
      }
    • Start build process by running buildWar from the command line:

      gradlew buildWar

      As a result, the app.war file will be created in the build\distributions\war project subdirectory.

  4. Use deployWar Gradle task to deploy your WAR file to Jelastic.

    task deployWar(type: CubaJelasticDeploy, dependsOn: buildWar){
        email = ****
        password = ****
        hostUrl = 'app.j.layershift.co.uk'
        environment = 'my-env-1'
    }
  5. After the deployment process is completed, your application will be available in the Jelastic cloud. To open it, use a URL like <environment>.<hostUrl> in the browser.

    For example:

    http://my-env-1.j.layershift.co.uk

    You can also open the application using the Open in Browser button located on the environments panel in Jelastic.

    jelasticDeploy

5.3.9. Deployment to Bluemix Cloud

CUBA Studio provides support of IBM® Bluemix® cloud deployment in a few easy steps.

Bluemix cloud deployment is currently applicable only to projects using PostgreSQL database. HSQLDB is available with in-process option only, that means the database will be recreated on every application restart, and the user data will be lost.

  1. Create an account on the Bluemix. Download and install:

    1. Bluemix CLI: http://clis.ng.bluemix.net/ui/home.html

    2. Cloud Foundry CLI: https://github.com/cloudfoundry/cli/releases

    3. Make sure the commands bluemix and cf work in the command line. If not, add your \IBM\Bluemix\bin path to the PATH environment variable.

  2. Create a Space in the Bluemix with any space name. You can group several applications within one space if needed.

  3. In the Space create an application server: Create AppCloudFoundry AppsTomcat.

  4. Specify the name of the application. The name should be unique as it will be used as part of the URL of your application.

  5. To create a Database service, click Create service in the Space dashboard and choose ElephantSQL.

  6. Open the application manager and connect the created DB Service to the application. Click Connect Existing. For the changes to take effect, the system requires restaging (updating) the application. In our case, it is not necessary, as the application will be redeployed.

  7. After the DB Service is connected, DB credentials become available with the View Credentials button. The DB properties are also stored in the VCAP_SERVICES environment variable of the application runtime and could be viewed by calling the cf env command. The created database is also accessible from outside of the Space, so you can work with it from your development environment.

  8. Setup your CUBA project to run with the PostgreSQL (the DBMS similar to the one you have in the Bluemix).

  9. Generate DB scripts and start the local Tomcat server. Make sure the application works.

  10. Generate WAR-file to deploy the application to Tomcat.

    1. Click Deployment > WAR Settings in the Project section of CUBA project view.

    2. Enable all the options using checkboxes, as for correct deployment it should be the Single WAR with JDBC driver and context.xml inside.

      bluemix war settings
    3. Click Generate button near the Custom context.XML field. In the opened dialog fill the credentials of the Database you have created in Bluemix.

      Use the credentials from uri of your DB service following the example below:

      {
        "elephantsql": [
          {
            "credentials": {
              "uri": "postgres://ixbtsvsq:F_KyeQjpEdpQfd4n0KpEFCYyzKAbN1W9@qdjjtnkv.db.elephantsql.com:5432/ixbtsvsq",
              "max_conns": "5"
            }
          }
        ]
      }

      Database user: ixbtsvsq

      Database password: F_KyeQjpEdpQfd4n0KpEFCYyzKAbN1W9

      Database URL: qdjjtnkv.db.elephantsql.com:5432

      Database name: ixbtsvsq

    4. Click Generate button to generate the custom web.xml file required for the single WAR.

    5. Save the settings. Generate the WAR-file using the buildWar Gradle task in Studio or command line.

      bluemix buildWar

      As a result, the app.war appears in the build/distributions/war/ sub-directory of the project.

  11. In the root directory of the project create manually the manifest.yml file. The contents of the file should be as follows:

    applications:
    - path: build/distributions/war/app.war
      memory: 1G
      instances: 1
      domain: eu-gb.mybluemix.net
      name: myluckycuba
      host: myluckycuba
      disk_quota: 1024M
      buildpack: java_buildpack
      env:
        JBP_CONFIG_TOMCAT: '{tomcat: { version: 8.0.+ }}'
        JBP_CONFIG_OPEN_JDK_JRE: '{jre: { version: 1.8.0_+ }}'

    where

    • path is the relative path to WAR-file.

    • memory: the default memory limit is 1G. You may want to allocate less or more memory to your application, this can also be done via Bluemix WEB interface. Note that the allocated memory affects the Runtime Cost.

    • name is the name of the Tomcat application you have created in the Cloud above (depends on your application location, see your App URL, for example, https://myluckycuba.eu-gb.mybluemix.net/).

    • host: the same as name.

    • env: the environment variables used to set the Tomcat and Java versions.

  12. In the command line switch to the root directory of your CUBA project.

    cd your_project_directory
  13. Connect to Bluemix (double check the domain name).

    cf api https://api.eu-gb.bluemix.net
  14. Log in to your Bluemix account.

    cf login -u your_bluemix_id -o your_bluemix_ORG
  15. Deploy your WAR to your Tomcat.

    cf push

    The push command gets all the required parameters from the manifest.yml file.

  16. You can find Tomcat server logs via Bluemix WEB-interface in the Logs tab on the application dashboard, as well as in command line using the command

    cf logs cuba-app --recent
  17. After the deployment process is completed, your application will become accessible in browser using the URL host.domain. This URL will be displayed in the ROUTE field in the table of your Cloud Foundry Apps.

5.3.10. Deployment to Heroku Cloud

The section describes how to deploy CUBA applications to the Heroku® cloud platform.

This tutorial covers deployment of a project using PostgreSQL database.

5.3.10.1. WAR Deployment to Heroku
Heroku account

First, create an account on Heroku using the web browser, free account hobby-dev is enough. Then login to the account and create new application using New button at the top of the page.

Select unique name (or left the field blank to assign automatically) and choose a server location. Now you have an application, for example, morning-beach-4895, this is the Heroku application name.

At the first time, you will be redirected to the Deploy tab. Use Heroku Git deployment method.

Heroku CLI
  • Install Heroku CLI on your computer.

  • Navigate to the folder containing your CUBA project. Further on we will use $PROJECT_FOLDER for it.

  • Open command prompt in $PROJECT_FOLDER and type:

    heroku login
  • Enter your credentials when prompted. From now on you don’t need to enter credentials for this project anymore.

  • Install Heroku CLI deployment plugin:

    heroku plugins:install heroku-cli-deploy
PostgreSQL database

Using the web browser go to Heroku data page

You can choose existent Postgres database or create one. Next steps describe how to create a new database.

  • Find Heroku Postgres block and click Create one

  • On the next screen click Install Heroku Postgr…​

  • Connect the database to Heroku application selected from a dropdown list

  • Select your Plan (for example: hobby-dev)

Alternatively, you can install PostgreSQL using Heroku CLI:

heroku addons:create heroku-postgresql:hobby-dev --app morning-beach-4895

Here morning-beach-4895 is your Heroku application name.

Now you can find the new database on the Resources tab. The database is connected to the Heroku application. To obtain database credentials go to the Datasource page of your Heroku database, scroll down to Administration section and click View credentials button.

Host compute.amazonaws.com
Database d2tk
User nmmd
Port 5432
Password 9c05
URI postgres://nmmd:9c05@compute.amazonaws.com:5432/d2tk
Project deployment settings
  • We assume that you use PostgreSQL with your CUBA project.

  • Open your CUBA project in Studio, navigate to CUBA Project Tree → Deployment, open WAR Settings dialog and then configure options as described below.

    • Select Build WAR

    • Set application home directory to '.' (dot)

    • Select Include JDBC driver

    • Select Include Tomcat’s context.xml

    • Click Generate button next to the Custom context.xml path field. Fill your database connection details in modal window.

    • Open the file generated modules/core/web/META-INF/war-context.xml and check connection params and credentials:

    • Select Single WAR for Middleware and Web Client

    • Click Generate button next to the Custom web.xml path field

    • Copy the code shown below and paste it into the App properties field:

      [
        'cuba.automaticDatabaseUpdate' : true
      ]
    • Save deployment settings and wait until Gradle project is refreshed.

Build WAR file

Build WAR file by double-clicking the new Build WAR project tree item or by executing the buildWar Gradle task:

gradlew buildWar
Application setup
  • Download Tomcat Webapp Runner from https://mvnrepository.com/artifact/com.github.jsimone/webapp-runner. The version of Webapp Runner must conform to the Tomcat version in use. For example, version 8.5.11.3 of Webapp Runner is suitable for Tomcat version 8.5.11. Rename JAR to webapp-runner.jar and place it into $PROJECT_FOLDER.

  • Download Tomcat DBCP from https://mvnrepository.com/artifact/org.apache.tomcat/tomcat-dbcp. Use the version corresponding to your Tomcat version, for example, 8.5.11. Create $PROJECT_FOLDER/libs, rename JAR to tomcat-dbcp.jar and place it into the $PROJECT_FOLDER/libs folder.

  • Create a file named Procfile in $PROJECT_FOLDER. The file should contain the following text:

    web: java $JAVA_OPTS -cp webapp-runner.jar:libs/* webapp.runner.launch.Main --enable-naming --port $PORT build/distributions/war/app.war
Git setup

Open the command prompt in $PROJECT_FOLDER and run the commands listed below:

git init
heroku git:remote -a morning-beach-4895
git add .
git commit -am "Initial commit"
Application deployment

Open the command prompt and run the following command:

On *nix:

heroku jar:deploy webapp-runner.jar --includes libs/tomcat-dbcp.jar:build/distributions/war/app.war --app morning-beach-4895

On Windows:

heroku jar:deploy webapp-runner.jar --includes libs\tomcat-dbcp.jar;build\distributions\war\app.war --app morning-beach-4895

Open the Resources tab in Heroku dashboard. A new Dyno should appear with a command from your Procfile:

heroku dyno

The application is deploying now. You can monitor logs to track the process.

Logs monitoring

Wait for a message https://morning-beach-4895.herokuapp.com/ deployed to Heroku in command window.

In order to track application logs, run the following command on the command line:

heroku logs --tail --app morning-beach-4895

After the deployment process is completed your application will be accessible in web browser by an URL like https://morning-beach-4895.herokuapp.com.

You can also open the application from the Heroku dashboard using the Open app button.

5.3.10.2. Deployment from GitHub to Heroku

This guide is intended for developers who have a CUBA project located on GitHub.

Heroku account

Create an account on Heroku using the web browser, free account hobby-dev is enough. Then login to the account and create new application using New button at the top of the page.

Select unique name (or left the field blank to assign automatically) and choose a server location. Now you have an application, for example, space-sheep-02453, this is a Heroku application name.

At the first time, you will be redirected to Deploy tab. Use GitHub deployment method. Follow the screen instructions how to authorize your GitHub account. Click Search button to list all available Git repositories then connect to desired repo. When your Heroku application is connected to GitHub you are able to activate Automatic Deploys. This allows you to redeploy Heroku application automatically on each Git push event. In this tutorial, the option is enabled.

Heroku CLI
  • Install Heroku CLI

  • Open command prompt in any folder of your computer and type:

    heroku login
  • Enter your credentials when prompted. From now on you don’t need to enter credentials for this project.

PostgreSQL database
  • Return to web browser with Heroku dashboard

  • Go to Resources tab

  • Click Find more add-ons button to find the database add-on

  • Find Heroku Postgres block and click it. Follow the instruction on the screen, click Login to install / Install Heroku Postgres.

Alternatively, you can install PostgreSQL using Heroku CLI:

heroku addons:create heroku-postgresql:hobby-dev --app space-sheep-02453

where space-sheep-02453 is your Heroku application name.

Now you can find the new database on the Resources tab. The database is connected to the Heroku application. To obtain database credentials go to the Datasource page of your Heroku database, scroll down to Administration section and click View credentials button.

Host compute.amazonaws.com
Database zodt
User artd
Port 5432
Password 367f
URI postgres://artd:367f@compute.amazonaws.com:5432/zodt
Project deployment settings
  • Navigate to your local CUBA project folder ($PROJECT_FOLDER)

  • Copy the content of modules/core/web/META-INF/context.xml to modules/core/web/META-INF/heroku-context.xml

  • Fill heroku-context.xml with your actual database connection details (see example below):

    <Context>
        <Resource driverClassName="org.postgresql.Driver"
                  maxIdle="2"
                  maxTotal="20"
                  maxWaitMillis="5000"
                  name="jdbc/CubaDS"
                  password="367f"
                  type="javax.sql.DataSource"
                  url="jdbc:postgresql://compute.amazonaws.com/zodt"
                  username="artd"/>
    
        <Manager pathname=""/>
    </Context>
Build configuration

Add the following Gradle task to your $PROJECT_FOLDER/build.gradle

task stage(dependsOn: ['setupTomcat', ':app-core:deploy', ':app-web:deploy']) {
    doLast {
        // replace context.xml with heroku-context.xml
        def src = new File('modules/core/web/META-INF/heroku-context.xml')
        def dst = new File('deploy/tomcat/webapps/app-core/META-INF/context.xml')
        dst.delete()
        dst << src.text

        // change port from 8080 to heroku $PORT
        def file = new File('deploy/tomcat/conf/server.xml')
        file.text = file.text.replace('8080', '${port.http}')

        // add local.app.properties for core application
        def coreConfDir = new File('deploy/tomcat/conf/app-core/')
        coreConfDir.mkdirs()
        def coreProperties = new File(coreConfDir, 'local.app.properties')
        coreProperties.text = ''' cuba.automaticDatabaseUpdate = true '''

        // rename deploy/tomcat/webapps/app to deploy/tomcat/webapps/ROOT
        def rootFolder = new File('deploy/tomcat/webapps/ROOT')
        if (rootFolder.exists()) {
            rootFolder.deleteDir()
        }

        def webAppDir = new File('deploy/tomcat/webapps/app')
        webAppDir.renameTo( new File(rootFolder.path) )

        // add local.app.properties for web application
        def webConfDir = new File('deploy/tomcat/conf/ROOT/')
        webConfDir.mkdirs()
        def webProperties = new File(webConfDir, 'local.app.properties')
        webProperties.text = ''' cuba.webContextName = / '''
    }
}
Procfile

A command that launches the application on Heroku side is passed by special file Procfile. Create a file named Procfile in $PROJECT_FOLDER with following text:

web: cd ./deploy/tomcat/bin && export 'JAVA_OPTS=-Dport.http=$PORT' && ./catalina.sh run

This provides JAVA_OPTS environment setting to Tomcat which starts with the Catalina script.

Premium addons

If your project uses CUBA Premium Add-ons, set additional variables for the Heroku application.

  • Open the Heroku dashboard.

  • Go to the Settings tab.

  • Expand the Config Variables section clicking the Reveal Config Vars button.

  • Add new Config Vars using your license key parts (separated by dash) as username and password:

CUBA_PREMIUIM_USER    | username
CUBA_PREMIUM_PASSWORD | password
Gradle wrapper

Your project requires Gradle wrapper. You can use CUBA Studio to add it: see the Build > Create or update Gradle wrapper main menu command.

  • Create the system.properties file in $PROJECT_FOLDER with the following content (example corresponds to local JDK 1.8.0_121 installed):

    java.runtime.version=1.8.0_121
  • Check that files Procfile, system.properties, gradlew, gradlew.bat and gradle are not in .gitignore

  • Add these files to repository and commit it

git add gradlew gradlew.bat gradle/* system.properties Procfile
git commit -am "Added Gradle wrapper and Procfile"
Application deployment

Once you commit and push all changes to GitHub, Heroku starts redeploying the application.

git push

The building process is available on the dashboard on the Activity tab. Click View build log link to track the build log.

After building process is completed, your application will become accessible in browser using the https://space-sheep-02453.herokuapp.com/. You can open the application from Heroku dashboard using the Open app button.

Logs monitoring

Heroku application log is shown by console command:

heroku logs --tail --app space-sheep-02453

Tomcat logs are also available in web application: Menu > Administration > Server Log

5.3.10.3. Container Deployment to Heroku

Set up UberJAR building as it is described in the Deployment with Docker section. Create a Heroku account and install Heroku CLI. For this purpose, please refer to the WAR Deployment to Heroku section.

Create the app and connect it to the database with the following command

heroku create cuba-sales-docker --addons heroku-postgresql:hobby-dev

After the task is completed you have to configure the database credentials in the jetty-env.xml file for the connection to the database created by Heroku.

  1. Go to the https://dashboard.heroku.com.

  2. Select your project, open the Resources tab and select the database.

  3. In the newly opened window open the Settings tab and click the View Credentials button.

Db

Switch to the IDE and open the jetty-env.xml file. You have to change the URL (host and database name), user name and password. Copy credentials from the site and paste them into the file.

<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC "-" "http://www.eclipse.org/jetty/configure_9_0.dtd">
<Configure id='wac' class="org.eclipse.jetty.webapp.WebAppContext">
    <New id="CubaDS" class="org.eclipse.jetty.plus.jndi.Resource">
        <Arg/>
        <Arg>jdbc/CubaDS</Arg>
        <Arg>
            <New class="org.apache.commons.dbcp2.BasicDataSource">
                <Set name="driverClassName">org.postgresql.Driver</Set>
                <Set name="url">jdbc:postgresql://<Host>/<Database></Set>
                <Set name="username"><User></Set>
                <Set name="password"><Password></Set>
                <Set name="maxIdle">2</Set>
                <Set name="maxTotal">20</Set>
                <Set name="maxWaitMillis">5000</Set>
            </New>
        </Arg>
    </New>
</Configure>

Build the single Uber JAR file using the Gradle task:

gradle buldUberJar

Also, you have to add some changes to the Dockerfile. First of all, if you use the free account, you have to restrict the amount of memory consumed by the application. Then you need to obtain the port of the app from the Heroku and add it to the image.

The Dockerfile should look like the following:

### Dockerfile

FROM openjdk:8

COPY . /usr/src/cuba-sales

CMD java -Xmx512m -Dapp.home=/usr/src/cuba-sales/home -jar /usr/src/cuba-sales/app.jar -port $PORT

Set up Git with the following commands:

git init
heroku git:remote -a cuba-sales-docker
git add .
git commit -am "Initial commit"

Then log in to the container registry. It’s the Heroku location for storing images.

heroku container:login

Next, build the image and push it to Container Registry

heroku container:push web

Here web is the process type of the application. When you run this command, by default Heroku is going to build the image using the Dockerfile in the current directory, and then push it to Heroku.

After the deployment process is completed, your application will be accessible in web browser by an URL like https://cuba-sales-docker.herokuapp.com/app

You can also open the application from the Heroku dashboard using the Open app button.

The third way to open a running application is to use the following command (remember to add the app context to the link, e.g. https://cuba-sales-docker.herokuapp.com/app):

heroku open

5.4. Proxy Configuration for Tomcat

For integration tasks you may need a proxy server. This part describes the configuration of Nginx HTTP-server as a proxy for CUBA application.

If you set up a proxy, do not forget to set cuba.webAppUrl value.

Tomcat Setup

If Tomcat is used behind a proxy server - it should be configured as well, so that Tomcat can properly dispatch proxy server’s headers.

First, add Valve to Tomcat configuration conf/server.xml, copy and paste the following code:

<Valve className="org.apache.catalina.valves.RemoteIpValve"
        remoteIpHeader="X-Forwarded-For"
        requestAttributesEnabled="true"
        internalProxies="127\.0\.0\.1"/>

There is another setting you should consider to change in the conf/server.xml file - AccessLogValve pattern attribute. Add %{x-forwarded-for}i to the pattern, so that Tomcat access log records both original source IP address and IP address(-es) of proxy server(s):

<Valve className="org.apache.catalina.valves.AccessLogValve"
    ...
    pattern="%h %{x-forwarded-for}i %l %u %t &quot;%r&quot; %s %b" />

Then restart Tomcat:

sudo service tomcat8 restart
NGINX

For Nginx there are 2 configurations described below. All examples were tested on Ubuntu 18.04.

For example, your web application works on http://localhost:8080/app.

Run command to install Nginx:

sudo apt-get install nginx

Navigate to http://localhost and ensure that Nginx works, you will see Nginx welcome page.

Now you may delete the symlink to default Nginx site:

rm /etc/nginx/sites-enabled/default

Next, configure your proxy one of the options selected below.

Direct Proxy

In this case the requests are handled by proxy, transparently passing to the application.

Create Nginx site configuration file /etc/nginx/sites-enabled/direct_proxy:

server {
    listen 80;
    server_name localhost;

    location /app/ {
        proxy_set_header Host               $host;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Forwarded-Proto  $scheme;

        # Required to send real client IP to application server
        proxy_set_header X-Forwarded-For    $proxy_add_x_forwarded_for;
        proxy_set_header X-Real-IP          $remote_addr;

        # Optional timeouts
        proxy_read_timeout      3600;
        proxy_connect_timeout   240;
        proxy_http_version      1.1;

        # Required for WebSocket:
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        proxy_pass              http://127.0.0.1:8080/app/;
    }
}

and restart Nginx

sudo service nginx restart

Now you can access your site via http://localhost/app.

Redirect to Path

This example describes how to change the application’s URL path from /app to /, as if the application were deployed in the root context (similar to /ROOT). This will allow you to access the application at http://localhost.

Create Nginx site configuration file /etc/nginx/sites-enabled/root_proxy:

server {
    listen 80;
    server_name localhost;

    location / {
        proxy_set_header Host               $host;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Forwarded-Proto  $scheme;

        # Required to send real client IP to application server
        proxy_set_header X-Forwarded-For    $proxy_add_x_forwarded_for;
        proxy_set_header X-Real-IP          $remote_addr;

        # Optional timeouts
        proxy_read_timeout      3600;
        proxy_connect_timeout   240;
        proxy_http_version      1.1;

        # Required for WebSocket:
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        proxy_pass              http://127.0.0.1:8080/app/;

        # Required for folder redirect
        proxy_cookie_path       /app /;
        proxy_set_header Cookie $http_cookie;
        proxy_redirect http://localhost/app/ http://localhost/;
    }
}

and restart Nginx

sudo service nginx restart

Now you can access your site via http://localhost.

Please note that similar deployment instructions are valid for Jetty, WildFly etc. You may need an additional configuration of those servers.

5.5. Proxy Configuration for Uber JAR

This part describes the configuration of Nginx HTTP-server as a proxy for CUBA Uber JAR application.

NGINX

For Nginx there are 2 configurations described below. All examples were tested on Ubuntu 16.04.

  1. Direct Proxy

  2. Redirect to Path

For example, your web application works on http://localhost:8080/app.

Uber JAR application uses Jetty 9.2 server. It is required to preconfigure Jetty in JAR to dispatch Nginx headers by Jetty.

Jetty Setup
  • Using Internal jetty.xml

    First, create Jetty configuration file jetty.xml in the root of your project, copy and paste the following code:

    <?xml version="1.0" encoding="utf-8"?>
    <!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_0.dtd">
    
    <Configure id="Server" class="org.eclipse.jetty.server.Server">
    
        <New id="httpConfig" class="org.eclipse.jetty.server.HttpConfiguration">
            <Set name="outputBufferSize">32768</Set>
            <Set name="requestHeaderSize">8192</Set>
            <Set name="responseHeaderSize">8192</Set>
    
            <Call name="addCustomizer">
                <Arg>
                    <New class="org.eclipse.jetty.server.ForwardedRequestCustomizer"/>
                </Arg>
            </Call>
        </New>
    
        <Call name="addConnector">
            <Arg>
                <New class="org.eclipse.jetty.server.ServerConnector">
                    <Arg name="server">
                        <Ref refid="Server"/>
                    </Arg>
                    <Arg name="factories">
                        <Array type="org.eclipse.jetty.server.ConnectionFactory">
                            <Item>
                                <New class="org.eclipse.jetty.server.HttpConnectionFactory">
                                    <Arg name="config">
                                        <Ref refid="httpConfig"/>
                                    </Arg>
                                </New>
                            </Item>
                        </Array>
                    </Arg>
                    <Set name="port">8080</Set>
                </New>
            </Arg>
        </Call>
    </Configure>

    Add webJettyConfPath property to the task buildUberJar in your build.gradle:

    task buildUberJar(type: CubaUberJarBuilding) {
        singleJar = true
        coreJettyEnvPath = 'modules/core/web/META-INF/jetty-env.xml'
        appProperties = ['cuba.automaticDatabaseUpdate' : true]
        webJettyConfPath = 'jetty.xml'
    }

    You may use Studio to generate jetty-env.xml by following Deployment > UberJAR Settings tab, or use an example below:

    <?xml version="1.0"?>
    <!DOCTYPE Configure PUBLIC "-" "http://www.eclipse.org/jetty/configure_9_0.dtd">
    <Configure id='wac' class="org.eclipse.jetty.webapp.WebAppContext">
        <New id="CubaDS" class="org.eclipse.jetty.plus.jndi.Resource">
            <Arg/>
            <Arg>jdbc/CubaDS</Arg>
            <Arg>
                <New class="org.apache.commons.dbcp2.BasicDataSource">
                    <Set name="driverClassName">org.postgresql.Driver</Set>
                    <Set name="url">jdbc:postgresql://<Host>/<Database></Set>
                    <Set name="username"><User></Set>
                    <Set name="password"><Password></Set>
                    <Set name="maxIdle">2</Set>
                    <Set name="maxTotal">20</Set>
                    <Set name="maxWaitMillis">5000</Set>
                </New>
            </Arg>
        </New>
    </Configure>

    Build Uber JAR using the following command:

    gradlew buildUberJar

    Your application will be located in build/distributions/uberJar, the default name is app.jar.

    Run your application:

    java -jar app.jar

    Then install and configure Nginx as described in Tomcat section.

    Depending on your schema, you can access your site via http://localhost/app or http://localhost URL.

  • Using External jetty.xml

    Use the same configuration file jetty.xml from the project root, as described above. Place it in your home folder and do not modify buildUberJar task in build.gradle.

    Build Uber JAR using the following command:

    gradlew buildUberJar

    Your application will be located in build/distributions/uberJar folder, default name is app.jar.

    First, run the application with a parameter -jettyConfPath:

    java -jar app.jar -jettyConfPath jetty.xml

    Then install and configure Nginx as described in Tomcat section.

    Depending on your schema and setings in jetty.xml file, you can access your site via http://localhost/app or http://localhost URL.

5.6. Application Scaling

This section describes ways to scale a CUBA application that consists of the Middleware and the Web Client for increased load and stronger fault tolerance requirements.

Stage 1. Both blocks are deployed on the same application server.

This is the simplest option implemented by the standard fast deployment procedure.

In this case, maximum data transfer performance between the Web Client and the Middleware is provided, because when the cuba.useLocalServiceInvocation application property is enabled, the Middleware services are invoked bypassing the network stack.

scaling_1

Stage 2. The Middleware and the Web Client blocks are deployed on separate application servers.

This option allows you to distribute load between two application servers and use server resources better. Furthermore, in this case, the load coming from web users has smaller effect on the other processes execution. Here, the other processes mean handling other client types, running scheduled tasks and, potentially, integration tasks which are performed by the middle layer.

Requirements for server resources:

  • Tomcat 1 (Web Client):

    • Memory size – proportional to the number of simultaneous users

    • CPU power – depends on the usage intensity

  • Tomcat 2 (Middleware):

    • Memory size – fixed and relatively small

    • CPU power – depends on the intensity of web client usage and of other processes

In this case and when more complex deployment options are used, the Web Client’s cuba.useLocalServiceInvocation application property should be set to false, and cuba.connectionUrlList property should contain the URL of the Middleware block.

scaling_2

Stage 3. A cluster of Web Client servers works with one Middleware server.

This option is used when memory requirements for the Web Client exceed the capabilities of a single JVM due to a large number of concurrent users. In this case, a cluster of Web Client servers (two or more) is started and user connection is performed through a Load Balancer. All Web Client servers work with one Middleware server.

Duplication of Web Client servers automatically provides fault tolerance at this level. However, the replication of HTTP sessions is not supported, in case of unscheduled outage of one of the Web Client servers, all users connected to it will have to login into the application again.

Configuration of this option is described in Setting up a Web Client Cluster.

scaling_3

Stage 4. A cluster of Web Client servers working with a cluster of Middleware servers.

This is the most powerful deployment option providing fault tolerance and load balancing for the Middleware and the Web Client.

Connection of users to the Web Client servers is performed through a load balancer. The Web Client servers work with a cluster of Middleware servers. They do not need an additional load balancer – it is sufficient to determine the list of URLs for the Middleware servers in the cuba.connectionUrlList application property. Another option is to use Apache ZooKeeper Integration Add-on for dynamic discovery of middleware servers.

Middleware servers exchange the information about user sessions, locks, etc. In this case, full fault tolerance of the Middleware is provided – in case of an outage of one of the servers, execution of requests from client blocks will continue on an available server without affecting users.

Configuration of this option is described in Setting up a Middleware Cluster.

scaling_4

5.6.1. Setting up a Web Client Cluster

This section describes the following deployment configuration:

cluster webclient

Servers host1 and host2 host Tomcat instances with the app web-app implementing the Web Client block. Users access the load balancer at http://host0/app, which redirects their requests to the servers. Server host3 hosts a Tomcat instance with the app-core web-app that implements the Middleware block.

5.6.1.1. Installing and Setting up a Load Balancer

Let us consider the installation of a load balancer based on Apache HTTP Server for Ubuntu 14.04.

  1. Install Apache HTTP Server and its mod_jk module:

    $ sudo apt-get install apache2 libapache2-mod-jk

  2. Replace the contents of the /etc/libapache2-mod-jk/workers.properties file with the following:

    workers.tomcat_home=
    workers.java_home=
    ps=/
    
    worker.list=tomcat1,tomcat2,loadbalancer,jkstatus
    
    worker.tomcat1.port=8009
    worker.tomcat1.host=host1
    worker.tomcat1.type=ajp13
    worker.tomcat1.connection_pool_timeout=600
    worker.tomcat1.lbfactor=1
    
    worker.tomcat2.port=8009
    worker.tomcat2.host=host2
    worker.tomcat2.type=ajp13
    worker.tomcat2.connection_pool_timeout=600
    worker.tomcat2.lbfactor=1
    
    worker.loadbalancer.type=lb
    worker.loadbalancer.balance_workers=tomcat1,tomcat2
    
    worker.jkstatus.type=status
  3. Add the lines listed below to /etc/apache2/sites-available/000-default.conf:

    <VirtualHost *:80>
    ...
        <Location /jkmanager>
            JkMount jkstatus
            Order deny,allow
            Allow from all
        </Location>
    
        JkMount /jkmanager/* jkstatus
        JkMount /app loadbalancer
        JkMount /app/* loadbalancer
    
    </VirtualHost>
  4. Restart the Apache HTTP service:

    $ sudo service apache2 restart

5.6.1.2. Setting up Web Client Servers

In the examples below, we provide paths to configuration files as if Fast Deployment in Tomcat is used.

On the Tomcat 1 and Tomcat 2 servers, the following settings should be applied:

  1. In tomcat/conf/server.xml, add the jvmRoute parameter equivalent to the name of the worker specified in the load balancer settings for tomcat1 and tomcat2:

    <Server port="8005" shutdown="SHUTDOWN">
      ...
      <Service name="Catalina">
        ...
        <Engine name="Catalina" defaultHost="localhost" jvmRoute="tomcat1">
          ...
        </Engine>
      </Service>
    </Server>
  2. Set the following application properties in tomcat/conf/app/local.app.properties:

    cuba.useLocalServiceInvocation = false
    cuba.connectionUrlList = http://host3:8080/app-core
    
    cuba.webHostName = host1
    cuba.webPort = 8080
    cuba.webContextName = app

    cuba.webHostName, cuba.webPort and cuba.webContextName parameters are not mandatory for WebClient cluster, but they allow easier identification of a server in other platform mechanisms, such as the JMX console. Additionally, Client Info attribute of the User Sessions screen shows an identifier of the Web Client that the current user is working with.

5.6.2. Setting up a Middleware Cluster

This section describes the following deployment configuration:

cluster mw

Servers host1 and host2 host Tomcat instances with the app web application implementing the Web Client block. Cluster configuration for these servers is described in the previous section. Servers host3 and host4 host Tomcat instances with the app-core web application implementing the Middleware block. They are configured to interact and share information about user sessions, locks, cache flushes, etc.

In the examples below, we provide paths to configuration files as if Fast Deployment in Tomcat is used.

5.6.2.1. Setting up Connection to the Middleware Cluster

In order for the client blocks to be able to work with multiple Middleware servers, the list of server URLs should be specified in the cuba.connectionUrlList application property. For the Web Client, this can be done in tomcat/conf/app/local.app.properties:

cuba.useLocalServiceInvocation = false
cuba.connectionUrlList = http://host3:8080/app-core,http://host4:8080/app-core

cuba.webHostName = host1
cuba.webPort = 8080
cuba.webContextName = app

A middleware server is randomly determined on the first remote connection for a user session, and it is fixed for the whole session lifetime ("sticky session"). Requests from anonymous session and without session do not stick to a server and go to random servers.

The algorithm of selecting a middleware server is provided by the cuba_ServerSorter bean which is by default implemented by the RandomServerSorter class. You can provide your own implementation in your project.

5.6.2.2. Configuring Interaction between Middleware Servers

Middleware servers can maintain shared lists of user sessions and other objects and coordinate invalidation of caches. cuba.cluster.enabled property should be enabled on each server to achieve this. Example of the tomcat/conf/app-core/local.app.properties file is shown below:

cuba.cluster.enabled = true

cuba.webHostName = host3
cuba.webPort = 8080
cuba.webContextName = app-core

For the Middleware servers, correct values of the cuba.webHostName, cuba.webPort and cuba.webContextName properties should be specified to form a unique Server ID.

Interaction mechanism is based on JGroups. The platform provides two configuration files for JGroups:

  • jgroups.xml - a UDP-based stack of protocols which is suitable for local network with enabled broadcast communication. This configuration is used by default when the cluster is turned on.

  • jgroups_tcp.xml - TCP-based stack of protocols which is suitable for any network. It requires explicit setting of cluster members addresses in TCP.bind_addr and TCPPING.initial_hosts parameters. In order to use this configuration, set cuba.cluster.jgroupsConfig application property.

In order to set up JGroups parameters for your environment, copy the appropriate jgroups.xml file from the root of cuba-core-<version>.jar to your project core module or to tomcat/conf/app-core and modify it.

ClusterManagerAPI bean provides the program interface for interaction between servers in the Middleware cluster. It can be used in the application – see JavaDocs and usages in the platform code.

5.6.2.3. Using ZooKeeper for Cluster Coordination

There is an application component that enables dynamic discovery of middleware servers for communication between middleware blocks and for requesting middleware from client blocks. It is based on integration with Apache ZooKeeper - a centralized service for maintaining configuration information. When this component is included in your project, you need to specify only one static address when running your application blocks - the address of ZooKeeper. Middleware servers will advertise themselves by publishing their addresses on the ZooKeeper directory and discovery mechanisms will request ZooKeeper for addresses of available servers. If a middleware server goes down, it will be automatically removed from the directory immediately or after a timeout.

The source code of application component is available on GitHub, the binary artifacts are published in the standard CUBA repositories. See README for information about including and configuring the component.

5.6.3. Server ID

Server ID is used for reliable identification of servers in a Middleware cluster. The identifier is formatted as host:port/context:

tezis.haulmont.com:80/app-core
192.168.44.55:8080/app-core

The identifier is formed based on the configuration parameters cuba.webHostName, cuba.webPort, cuba.webContextName, therefore it is very important to specify these parameters for the Middleware blocks working within the cluster.

Server ID can be obtained using the ServerInfoAPI bean or via the ServerInfoMBean JMX interface.

5.7. Using JMX Tools

This section describes various aspects of using Java Management Extensions in CUBA-based applications.

5.7.1. Built-In JMX Console

The Web Client module of the cuba application component contains JMX objects viewing and editing tool. The entry point for this tool is com/haulmont/cuba/web/app/ui/jmxcontrol/browse/display-mbeans.xml screen registered under the jmxConsole identifier and accessible via Administration > JMX Console in the standard application menu.

Without extra configuration, the console shows all JMX objects registered in the JVM where the Web Client block of the current user is running. Therefore, in the simplest case, when all application blocks are deployed to one web container instance, the console has access to the JMX beans of all tiers as well as the JMX objects of the JVM itself and the web container.

Names of the application beans have a prefix corresponding to the name of the web-app that contains them. For example, the app-core.cuba:type=CachingFacade bean has been loaded by the app-core web-app implementing the Middleware block, while the app.cuba:type=CachingFacade bean has been loaded by the app web-app implementing the Web Client block.

jmx console
Figure 52. JMX Console

JMX console can also work with the JMX objects of a remote JVM. This is useful when application blocks are deployed over several instances of a web container, for example, separate Web Client and Middleware.

To connect to a remote JVM, a previously created connection should be selected in the JMX Connection field of the console, or a new connection can be created:

jmx connection edit
Figure 53. Editing a JMX Connection

To get a connection, JMX host, port, login, and password should be specified. There is also the Host name field, which is populated automatically if any CUBA-application block is detected at the specified address. In this case, the value of this field is defined as the combination of cuba.webHostName and cuba.webPort properties of this block, which enables identifying the server that contains it. If the connection is done to a 3rd party JMX interface, then the Host name field will have the "Unknown JMX interface" value. However, it can be changed arbitrarily.

In order to allow a remote JVM connection, the JVM should be configured properly (see below).

5.7.2. Setting up a Remote JMX Connection

This section describes Tomcat startup configuration required for a remote connection of JMX tools.

5.7.2.1. Tomcat JMX for Windows
  • Edit bin/setenv.bat in the following way:

    set CATALINA_OPTS=%CATALINA_OPTS% ^
    -Dcom.sun.management.jmxremote ^
    -Djava.rmi.server.hostname=192.168.10.10 ^
    -Dcom.sun.management.jmxremote.ssl=false ^
    -Dcom.sun.management.jmxremote.port=7777 ^
    -Dcom.sun.management.jmxremote.authenticate=true ^
    -Dcom.sun.management.jmxremote.password.file=../conf/jmxremote.password ^
    -Dcom.sun.management.jmxremote.access.file=../conf/jmxremote.access

    Here, the java.rmi.server.hostname parameter should contain the actual IP address or the DNS name of the computer where the server is running; com.sun.management.jmxremote.port sets the port for JMX tools connection.

  • Edit the conf/jmxremote.access file. It should contain user names that will be connecting to the JMX and their access level. For example:

    admin readwrite
  • Edit the conf/jmxremote.password file. It should contain passwords for the JMX users, for example:

    admin admin
  • The password file should have reading permissions only for the user running the Tomcat. server. You can configure permissions the following way:

    • Open the command line and go to the conf folder

    • Run the command:

      cacls jmxremote.password /P "domain_name\user_name":R

      where domain_name\user_name is the user’s domain and name

    • After this command is executed, the file will be displayed as locked (with a lock icon) in Explorer.

  • If Tomcat is installed as a Windows service, then the service should be started on behalf of the user who has access permissions for jmxremote.password. It should be kept in mind that in this case the bin/setenv.bat file is ignored and the corresponding JVM startup properties should be specified in the application that configures the service.

5.7.2.2. Tomcat JMX for Linux
  • Edit bin/setenv.sh the following way:

    CATALINA_OPTS="$CATALINA_OPTS -Dcom.sun.management.jmxremote \
    -Djava.rmi.server.hostname=192.168.10.10 \
    -Dcom.sun.management.jmxremote.port=7777 \
    -Dcom.sun.management.jmxremote.ssl=false \
    -Dcom.sun.management.jmxremote.authenticate=true"
    
    CATALINA_OPTS="$CATALINA_OPTS -Dcom.sun.management.jmxremote.password.file=../conf/jmxremote.password -Dcom.sun.management.jmxremote.access.file=../conf/jmxremote.access"

    Here, the java.rmi.server.hostname parameter should contain the real IP address or the DNS name of the computer where the server is running; com.sun.management.jmxremote.port sets the port for JMX tools connection

  • Edit conf/jmxremote.access file. It should contain user names that will be connecting to the JMX and their access level. For example:

    admin readwrite
  • Edit the conf/jmxremote.password file. It should contain passwords for the JMX users, for example:

    admin admin
  • The password file should have reading permissions only for the user running the Tomcat server. Permissions for the current user can be configured the following way:

    • Open the command line and go to the conf folder.

    • Run the command:

      chmod go-rwx jmxremote.password

5.8. Server Push Settings

CUBA applications use server push technology in the Background Tasks mechanism. It may require an additional setup of the application and proxy server (if any).

By default, server push uses the WebSocket protocol. The following application properties affect the platform server push functionality:

The information below is obtained from the Vaadin website - Configuring push for your environment.

Chrome says ERR_INCOMPLETE_CHUNKED_ENCODING

This is completely normal and means that the (long-polling) push connection was aborted by a third party. This typically happens when there is a proxy between the browser and the server and the proxy has a configured timeout and cuts the connection when the timeout is reached. The browser should reconnect to the server normally after this happens.

Tomcat 8 + Websockets
java.lang.ClassNotFoundException: org.eclipse.jetty.websocket.WebSocketFactory$Acceptor

This implies you have Jetty deployed on the classpath somewhere. Atmosphere gets confused and tries to use its Websocket implementation instead of Tomcat’s. One common reason for this is that you have accidentally deployed vaadin-client-compiler, which has Jetty as a dependency (needed by SuperDevMode for instance).

Glassfish 4 + Streaming

Glassfish 4 requires the comet option to be enabled for streaming to work.

Set

(Configurations → server-config → Network Config → Protocols → http-listener-1 → HTTP → Comet Support)

or use

asadmin set server-config.network-config.protocols.protocol.http-listener-1.http.comet-support-enabled="true"
Glassfish 4 + Websockets

If you are using Glassfish 4.0, upgrade to Glassfish 4.1 to avoid problems.

Weblogic 12 + Websockets

Use WebLogic 12.1.3 or newer. WebLogic 12 specifies a timeout of 30 sec by default for websocket connections. To avoid constant reconnects, you can set the weblogic.websocket.tyrus.session-max-idle-timeout init parameter to either -1 (no timeout in use) or a higher value than 30000 (value is in ms).

JBoss EAP 6.4 + Websockets

JBoss EAP 6.4 includes support for websockets but they are disabled by default. To make websockets work you need to change JBoss to use the NIO connector by running:

$ bin/jboss-cli.sh --connect

and the following commands:

batch
/subsystem=web/connector=http/:write-attribute(name=protocol,value=org.apache.coyote.http11.Http11NioProtocol)
run-batch
:reload

Then add a WEB-INF/jboss-web.xml to you war file with the following contents to enable websockets:

<jboss-web version="7.2" xmlns="http://www.jboss.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.jboss.com/xml/ns/javaee schema/jboss-web_7_2.xsd">
    <enable-websockets>true</enable-websockets>
</jboss-web>
Duplicate resource

If server logs contain

Duplicate resource xyz-abc-def-ghi-jkl. Could be caused by a dead connection not detected by your server. Replacing the old one with the fresh one

This indicates that first, the browser connected to the server and used the given identifier for the push connection. Everything went as expected. Later on, a browser (probably the same one) connected again using the same identifier but according to the server, the old browser connection should still be active. The server closes the old connection and logs the warning.

This happens because typically there was a proxy between the browser and the server, and the proxy was configured to kill open connections after a certain inactivity timeout on the connection (no data is sent before the server issues a push command). Because of how TCP/IP works, the server has no idea that the connection has been killed and continues to think that the old client is connected and all is well.

You have a couple of options to avoid this problem:

  1. If you are in control of the proxy, configure it not to timeout/kill push connections (connections to the /PUSH url).

  2. If you know what the proxy timeout is, configure a slightly shorter timeout for push in the application so that the server terminates the idle connection and is aware of the termination before the proxy can kill the connection.

    1. Set the cuba.web.pushLongPolling parameter to true to enable long polling transport instead of websocket.

    2. Use the cuba.web.pushLongPollingSuspendTimeoutMs parameter to set push timeout in milliseconds.

If you do not configure the proxy so that the server knows when the connection is killed, you also have a small chance of losing pushed data. If it so happens that the server does a push right after the connection was killed, it will not realize that it pushed data into a closed connection (because of how sockets work and especially how they work in Java). Disabling the timeout or setting the timeout on the server also resolves this potential issue.

Using Proxy

If users connect to the application server via a proxy that does not support WebSocket, set cuba.web.pushLongPolling to true and increase proxy request timeout to 10 minutes or more.

Below is an example of the Nginx web server settings for using WebSocket:

location / {
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-Server $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_read_timeout     3600;
    proxy_connect_timeout  240;
    proxy_set_header Host $host;
    proxy_set_header X-RealIP $remote_addr;

    proxy_pass http://127.0.0.1:8080/;
    proxy_set_header X-Forwarded-Proto $scheme;

    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
}

5.9. Health Check URL

Every application block deployed as a web application provides a health check URL. An HTTP GET request for this URL returns ok if the block is ready to work.

The URL paths for different blocks are listed below:

  • Middleware: /remoting/health

  • Web Client: /dispatch/health

  • Web Portal: /rest/health (requires REST API add-on)

So for an application named app and deployed at localhost:8080 the URLs will be:

  • http://localhost:8080/app-core/remoting/health

  • http://localhost:8080/app/dispatch/health

  • http://localhost:8080/app-portal/rest/health

You can replace ok in response with any text using the cuba.healthCheckResponse application property.

The health check controllers also send an application event of type HealthCheckEvent. Therefore you can add your own logic of checking the application health. In the example on GitHub, a bean of the web tier listens to the health check events and invokes a middleware service, which in turn executes an operation on the database.

5.10. Creating and Updating Database in Production

This section describes different ways of creating and updating a database during application deployment and operation. To learn more about the structure of database scripts, see Scripts to Create and Update Database.

5.10.1. Execution of Database Scripts by Server

The execution of DB scripts by server mechanism can be used for both database initialization and its further update during the application development and data schema modification.

The following actions should be completed to initialize a new database:

  • Enable the cuba.automaticDatabaseUpdate application property by adding the following line to the local.app.properties file of the Middleware block:

    cuba.automaticDatabaseUpdate = true

    For fast deployment to Tomcat, this file is located in the tomcat/conf/app-core directory. If the file does not exist, create it.

  • Create an empty database corresponding to the URL specified in the data source description in context.xml.

  • Start the application server containing the Middleware block. At application start, the database will be initialized and ready for work.

After that, each time when the application server starts, a scripts execution mechanism will compare the set of scripts located in the database scripts directory with the list of already executed scripts registered in the database. If new scripts are found, they will be executed and registered as well. Typically it is enough to include the update scripts in each new application version, and the database will be actualized each time when the application server is restarted.

When using the database scripts execution mechanism at server start, the following should be considered:

  • If any error occurs when running a script, the Middleware block stops initialization and becomes inoperable. The client blocks generate messages about inability to connect to the Middleware.

    Check the app.log file located in the server’s log folder for a message about SQL execution from the com.haulmont.cuba.core.sys.DbUpdaterEngine logger and, possibly, further error messages to identify the error reasons.

  • The update scripts, as well as the DDL and the SQL commands within the scripts separated with "^", are executed in separate transactions. That is why when an update fails there is still a big chance that a part of the scripts or even individual commands of the last script will have been executed and committed to the database.

    With this in mind, creating a backup copy of the database immediately before starting the server is highly recommended. Then, when the error reason is fixed, the database can be restored and the automatic process restarted.

    If the backup is missing, you should identify which part of the script was executed and committed after the error is fixed. If the entire script failed to execute, the automatic process can be simply restarted. If some of the commands before the erroneous one were separated with the "^" character, executed in a separate transaction and committed, then the remaining part of the commands should be run and this script should be registered in SYS_DB_CHANGELOG manually. After that, the server can be started and the automatic update mechanism will start processing the next unexecuted script.

    CUBA Studio generates update scripts with ";" delimiter for all database types except Oracle. If update script commands are separated by semicolons, the script is executed in one transaction and entirely rolled back in case of failure. This behavior ensures consistency between the database schema and the list of executed update scripts.

5.10.2. Initializing and Updating Database from Command Line

Database create and update scripts can be run from the command line using the com.haulmont.cuba.core.sys.utils.DbUpdaterUtil class included in the platform’s Middleware block. At startup, the following arguments should be specified:

  • dbTypeDBMS type, possible values: postgres, mssql, oracle, mysql.

  • dbVersionDBMS version (optional argument).

  • dbDriver - JDBC driver class name (optional argument). If not provided, the driver class name will be derived from dbType.

  • dbUser – database user name.

  • dbPassword – database user password.

  • dbUrl – database connection URL. For primary initialization, the specified database should be empty; the database is not cleared automatically in advance.

  • scriptsDir – absolute path to the folder containing scripts in the standard structure. Typically, this is the database scripts directory supplied with the application.

  • one of the possible commands:

    • create – initialize the database.

    • check – show all unexecuted update scripts.

    • update – update the database.

An example of a script for Linux running DbUpdaterUtil:

#!/bin/sh

DB_URL="jdbc:postgresql://localhost/mydb"

APP_CORE_DIR="./../webapps/app-core"
WEBLIB="$APP_CORE_DIR/WEB-INF/lib"
SCRIPTS="$APP_CORE_DIR/WEB-INF/db"
TOMCAT="./../lib"
SHARED="./../shared/lib"

CLASSPATH=""
for jar in `ls "$TOMCAT/"`
do
  CLASSPATH="$TOMCAT/$jar:$CLASSPATH"
done

for jar in `ls "$WEBLIB/"`
do
  CLASSPATH="$WEBLIB/$jar:$CLASSPATH"
done

for jar in `ls "$SHARED/"`
do
  CLASSPATH="$SHARED/$jar:$CLASSPATH"
done

java -cp $CLASSPATH com.haulmont.cuba.core.sys.utils.DbUpdaterUtil \
 -dbType postgres -dbUrl $DB_URL \
 -dbUser $1 -dbPassword $2 \
 -scriptsDir $SCRIPTS \
 -$3

This script is designed to work with the database named mydb running on the local PostgreSQL server. The script should be located in the bin folder of the Tomcat server and should be started with {username}, {password} and {command}, for example:

./dbupdate.sh cuba cuba123 update

Script execution progress is displayed in the console. If any error occurs, same actions as described in the previous section for the automatic update mechanism should be performed.

When updating the database from the command line, the existing Groovy scripts are started, but only their main part gets executed. Due to the lack of the server context, the script’s PostUpdate part is ignored with the corresponding message written to the console.

6. Security Subsystem

CUBA includes a sophisticated security subsystem that solves common problems of enterprise applications:

  • Authentication using built-in users repository, LDAP, SSO or social networks.

  • The role-based access control for the data model (entity operations and attributes), UI screens and arbitrary named permissions. For example, John Doe can view documents, but cannot create, update or delete them. He also can view all document attributes except amount.

  • Row-level data access control - ability to specify rights to particular entity instances. For example, John Doe can view documents that have been created in his department only.

6.1. Web Security

Are CUBA applications secure?

CUBA as a framework follows good security practices and provides you with automatic protection against some of the most common vulnerabilities in web applications. Its architecture promotes a secure programming model, allowing you to concentrate on your business and application logic.

1. UI State and Validation

Web Client is a server-side application, where all of your application state, business and UI logic resides on the server. Unlike client driven frameworks, Web Client never exposes its internals to the browser, where vulnerabilities can be leveraged by an attacker. The data validation is always done on the server and therefore cannot be by-passed with client-side attacks, the same applies to REST API validation.

2. Cross-Site Scripting (XSS)

Web Client has built-in protection against cross-site scripting (XSS) attacks. It converts all data to use HTML entities before the data is rendered in the user’s browser.

3. Cross-Site Request Forgery (CSRF)

All requests between the client and the server are included with a user session specific CSRF token. All communication between the server and the client is handled by Vaadin Framework, so you do not need to remember to include the CSRF tokens manually.

4. Web Services

All communication in Web Client goes through one web service used for RPC requests. You never open up your business logic as web services and thus there are less attack entry points to your application.

Generic REST API automatically applies roles, permissions and all security constraints for both logged in and anonymous users.

5. SQL Injection

The platform uses ORM layer based on EclipseLink that is protected against SQL injections. Parameters of SQL queries are always passed to JDBC as parameters array and not interpolated into SQL queries as raw strings.

6.2. Security Subsystem Components

The main CUBA security subsystem components are shown in the diagram below.

Security
Figure 54. Security Subsystem Components Diagram

Below is an overview of these components.

Security management screens – screens available to system administrator for configuring user access rights.

Login screen − system login window. This window provides user authentication by username and password. The database stores password hashes for security.

The UserSession object is created upon login. This is the central security element associated with the currently authenticated user and containing information on data access rights.

The user login process is described in the Login section.

Roles − user roles. A role is an object which defines a set of permission. A user can have multiple roles.

Access Groups − user access groups. The groups have a hierarchical structure, with each element defining a set of constraints, allowing to control access to individual entity instances (at table row level).

6.2.1. Login Screen

The login screen provides the ability to register within the system with a login name and password. The login name is case-insensitive.

The Web Client’s Remember Me checkbox can be configured by using the cuba.web.rememberMeEnabled application property. The drop-down list of supported languages on the standard login screen can be configured with the cuba.localeSelectVisible and cuba.availableLocales application properties.

In Web Client, the standard login window can be customized or completely replaced in the project using CUBA Studio. Select Generic UI in the project tree and click New > Screen in the context menu. Then select the Login window template on the Legacy Screen Templates tab. The new ext-loginWindow.xml file will be created in the Web module and automatically registered in the web-screens.xml. See also Generic UI Infrastructure.

The platform has a mechanism for the protection against password brute force cracking: see the cuba.bruteForceProtection.enabled application property.

For deeper customization of the authentication procedure, see Login and Web Login sections.

The appearance of the Login Screen can be customized using SCSS variables with $cuba-login-* prefix. You can change these variables in the visual editor after creating a theme extension or a custom theme.

6.2.2. Users

Each system user has a corresponding instance of sec$User entity, containing unique login, password hash, reference to access group and list of roles, and other attributes. User management is carried out using the Administration > Users screen:

security user browser

In addition to the standard actions to create, update and delete records, the following actions are available:

  • Copy – quickly creates a new user based on the selected one. The new user will have the same access group and role set. Both can be changed in the new user edit screen.

  • Copy settings – enables copying user interface settings from one user to several others. The settings include the table presentations, the SplitPanel separator position, filters and search folders.

  • Change password – allowing changing password for a selected user.

  • Reset passwords – enables performing the following actions on selected users:

    • If Generate new passwords flag is not selected in the Reset passwords for selected users dialog, the Change password at next logon flag will be assigned to selected users. These users will be asked to change the password on the next successful login.

      In order to be able to change their passwords, the users must have permission to the sec$User.changePassword screen. Keep it in mind when configuring roles, especially if you assign a Denying role to all users. The screen can be found under the Other screens entry in the role editor.

    • If Generate new passwords flag is selected, new random passwords will be generated for selected users and displayed to the system administrator. The list of passwords can be exported to XLS and sent to related users. Additionally, the Change password at next logon flag will be set for each user, ensuring that users change the password on next login.

    • If Send emails with generated passwords flag is selected in addition to Generate new passwords, the automatically generated one-time passwords will be sent to corresponding users directly, and not shown to system administrator. The sending is done asynchronously so it requires a special scheduled task mentioned in section Sending Methods.

      The template of password reset emails can be modified. Create custom templates in the core module, taking reset-password-subject.gsp and reset-password-body.gsp as an example. In order to localize templates, if needed, create files with locale suffixes, as it is done in the platform.

      The templates are based on Groovy SimpleTemplateEngine syntax, thus you can use Groovy blocks inside of the template content. For instance:

      Hello <% print (user.active ? user.login : 'user') %>
      
      <% if (user.email.endsWith('@company.com')) { %>
      
      The password for your account has been reset.
      
      Please login with the following temporary password:
      ${password}
      and immediately create a new one for the further work.
      
      Thank you
      
      <% } else {%>
      
      Please contact your system administrator for a new password.
      
      <%}%>

      Data binding for these templates contains user, password, and persistence variables. You can also use any Spring beans of the middleware if you import AppBeans class and get them using AppBeans.get() method.

      Having overridden the templates, in the app.properties of the core module specify the following properties:

      cuba.security.resetPasswordTemplateBody = <relative path to your file>
      cuba.security.resetPasswordTemplateSubject = <relative path to your file>

      For easier configuration in production, the templates can also be located or overridden in the configuration directory, with the properties added to the local.app.properties file.

The user edit screen is described below:

security user editor
  • Login – unique login name (required).

  • Groupaccess group.

  • Last name, First name, Middle name – parts of user’s full name.

  • Name – automatically generated user’s full name. Based on full name parts above and a rule defined in the cuba.user.fullNamePattern application property. The name can also be changed manually.

  • Position – job position.

  • Language – the user interface language that will be set for the user, if the ability to choose a language on login is turned off using the cuba.localeSelectVisible application property.

  • Time Zone – the time zone that will be used for displaying and entering timestamp values.

  • Email – email address.

  • Active – if not set, the user is unable to login to the system.

  • Permitted IP Mask – IP address mask, defining addresses from which the user is allowed to login.

    The mask is a list of IP addresses, separated by commas. Both the IPv4 and IPv6 address formats are supported. IPv4 address should consist of four numbers separated with periods. IPv6 address represents eight groups of four hexadecimal characters separated with colons. The "*" symbol can be used in place of an address part, to match any value. Only one type of address format (IPv4 or IPv6) can be used in the mask at the same time.

    Example: 192.168.*.*

  • Roles – user roles list.

  • Substituted Userssubstituted users list.

6.2.2.1. User Substitution

The system administrator can give a user an ability to substitute another user. The substituting user will have the same session, but a different set of roles, constraints and attributes, assigned from the substituted user.

It is recommended to use the UserSession.getCurrentOrSubstitutedUser() method for retrieving the current user in the application code, which returns the substituted user, if there is an active substitution. The platform audit mechanisms (the createdBy and updatedBy attributes, change log and entity snapshots) always register the real logged-in user.

If the user has substituted users, a drop-down list will be shown in the application upper right corner instead of the plain text with the current user name:

user subst select

If another user is selected in this list, all opened screens will be closed and the substitution will be made active. The UserSession.getUser() method will still return the user that has logged in, however, the UserSession.getSubstitutedUser() method will return the substituted user. If there is no substitution, the UserSession.getSubstitutedUser() method will return null.

Substituted users can be managed through the Substituted Users table in the user edit screen. The user substitution screen is described below:

user subst edit
  • User – the edited user. This user will substitute another user.

  • Substituted user – the substituted user.

  • Start date, End date – optional substitution period. User substitution will be unavailable outside of this period. If no period is specified, substitution will be available until this table entry is removed.

6.2.2.2. Time Zone

By default, all temporal values are displayed in the server’s time zone. The server’s time zone is the one returned by TimeZone.getDefault() method of an application block. This default time zone is typically obtained from the operating system but can be set explicitly by user.timezone Java system property. For example, to set the time zone to GMT for web client and middleware running on Tomcat under Unix, add the following line to tomcat/bin/setenv.sh file:

CATALINA_OPTS="$CATALINA_OPTS -Duser.timezone=GMT"

A user can view and edit timestamp values in a time zone different from the server’s time zone. There are two ways to manage user’s time zone:

  • An administrator can do it in the User editor screen.

  • The user can change their time zone in the Help > Settings window.

In both cases, the time zone settings consist of two fields:

  • Time zone name dropdown allows a user to select the time zone explicitly.

  • Auto checkbox indicates that the time zone will be obtained from the current environment (web browser for the web client).

If both fields are empty, no time zone conversions are performed for the user. Otherwise, the platform saves time zone in the UserSession object when the user logs in and uses it for displaying and entering timestamp values. The application code can also use the value returned by UserSession.getTimeZone() for custom functionality.

If a time zone is in use for the current session, its short name and offset from GMT are displayed in the application main window next to the current user’s name.

Time zone conversions are performed only for DateTimeDatatype entity attributes, i.e., timestamps. Attributes storing date (DateDatatype) and time (TimeDatatype) separately are not affected. You can also deny conversions for a timestamp attribute by annotating it with @IgnoreUserTimeZone.

6.2.3. Permissions

The permission determines the user’s right to any system object or functionality, such as screen, entity operation, etc. The permission can either grant the user the right to the object, or revoke it (in essence, it is actually a prohibition).

By default, the user has the right to an object, unless explicitly denied by a permission.

The permissions are granted by the sec$Permission entity instances and contain the following attributes:

  • type – permission type: determines the object type the permission is imposed on.

  • target – permission object: determines the specific object the permission is imposed on. The format of the attribute depends on the permission type.

  • value – permission value. The value range depends on the permission type.

The permission types are described below:

  • PermissionType.SCREEN – screen permission.

    The screen identifier should be specified in the target attribute; the value attribute can be 0 or 1 (the screen is denied or allowed, respectively).

    The screen permissions are checked when building the system main menu and with each invocation of the openWindow(), openEditor(), openLookup() methods of the Frame interfaces.

    To check the screen permission in the application code, use the isScreenPermitted() method of the Security interface.

  • PermissionType.ENTITY_OP – entity operation permission.

    The entity name should be specified in the target attribute, followed by a colon, and then an operation type: create, read, update, delete. For example: library_Book:delete. The value attribute can be 0 or 1 (the operation is denied or allowed, respectively).

    See the Data Access Checks section for how entity operation permissions are used by different mechanisms of the framework.

    To check the entity operation permission in the application code, use the isEntityOpPermitted() method of the Security interface.

  • PermissionType.ENTITY_ATTR – entity attribute permission.

    The entity name should be specified in the target attribute, followed by a colon, and then an attribute name. For example: library_Book:name. The value attribute can be 0, 1 or 2 (the attribute is hidden, read-only or read-write, respectively).

    See the Data Access Checks section for how entity attribute permissions are used by different mechanisms of the framework.

    To check the entity attribute permission in the application code, use the isEntityAttrPermitted() method of the Security interface.

  • PermissionType.SPECIFIC – permission on an arbitrary named functionality. Specific permissions can be used instead of roles for binary permitting/denying some project-specific functionality, as roles are designed for aggregating permissions.

    The functionality identifier should be specified in the target attribute; the value attribute can be 0 or 1 (denied or allowed, respectively).

    Specific permissions for this project are set in the configuration file permissions.xml.

    For example:

    @Inject
    private Security security;
    
    private void calculateBalance() {
        if (!security.isSpecificPermitted("myapp.calculateBalance"))
            return;
        ...
    }
  • PermissionType.UI – arbitrary screen component permission.

    The screen identifier should be specified in the target attribute, followed by a colon, and then a component path. The format of the component path is described in the next section.

To check permissions, instead of directly using methods of the UserSession class, it is recommended to use the same methods of Security interface that works with possible entity extension.

6.2.4. Roles

The role combines a set of permissions that can be granted to the user.

The user may have several roles, in which case a logical sum (OR) is devised from all of the assigned roles. For example, if a user has roles A, B and C, role A denies X, role B allows X, role C does not set explicit permissions on X, then X will be allowed.

If no user roles explicitly define permission on the object, the user will have the permission for this object. Therefore, the users have rights to all the objects if they have no roles that explicitly define the permission, or have at least one role that grants the permission.

If a user has a single role without explicitly set permissions, or does not have any roles at all, he will have all rights to all objects.

The role list is displayed in the Administration > Roles screen. In addition to the standard actions to create, update, and delete records, the screen has the Assign to users button, allowing assigning the selected role to multiple users.

The role edit screen is described below. The role attributes are displayed in the upper part:

role attributes
  • Name – unique role name or id (required). The name cannot be changed after the role has been created.

  • Localized name – user-friendly role name.

  • Description – arbitrary role description.

  • Type – role type, can be:

    • Standard – the role of this type grants only explicitly set permissions.

    • Super – the role of this type automatically grants all permissions. It should be assigned to system administrators, since it removes all prohibitions set by other roles.

    • Read-only – the role of this type automatically denies the permissions for the following entity operations: CREATE, UPDATE, DELETE. Therefore, the user with this role can only read the data and is unable to update it (unless there are other user roles explicitly allowing these operations).

    • Denying – the role of this type automatically denies the permissions for all objects, except entity attributes. In order to view or update something in the system, the user should be assigned an additional role that explicitly gives the necessary rights.

      Permissions can be explicitly set for all the role types; for example, you can add the permissions to modify entities for the Read-only role. However, it does not make sense to prohibit anything for the Super role, because this special role type removes all prohibitions.

  • Default role – default role flag. All roles with this flag are automatically assigned to the newly created users.

The permission management tabs are described below.

  • The Screens tab configures screen permissions:

    role screen permissions

    The tree in the left part of the tab reflects the structure of the application’s main menu. The last tree element is Other screens, which contains screens without a main menu item (for example, entity edit screens).

  • The Entities tab – configures entity operation permissions:

    role entity permissions

    The Assigned only is selected by default, so that the table contains only the entities that have explicit permissions in this role. Therefore, the table for a new role will be empty. In order to add permissions, uncheck Assigned only and click Apply. The entity list can be filtered by entering a part of an entity name in the Entity field and clicking Apply.

    System level checkbox enables viewing and selecting system entities marked with the @SystemLevel annotation, which are not shown by default.

    When a constraint of this kind is violated, the error notification will be shown to the user. To localize the error messages, you should override the localization keys for the RowLevelSecurityExceptionHandler in the main message pack.

  • The Attributes tab – configures entity attribute permissions:

    role attr permissions

    The Permissions column in the entity table shows the list of the attributes that have explicit permissions. The modify (full access) permissions are marked with green, read-only (read-only) – with blue, hide (the attribute is hidden) – with red.

    Entity list can be managed similarly to the list in the Entities tab.

    To change the access to attributes dynamically depending also on the current state of the entity or its linked entities, use the Entity Attribute Access Control mechanism with the SetupAttributeAccessHandler interface.

  • The Specific tab configures named functionality permissions:

    role specific permissions

    The permissions.xml project configuration file defines the object names to which specific permissions can be assigned.

  • The UI tab configures UI screen component permissions:

    role ui permissions

    The permissions on this screen allow restricting access to any screen component, including the ones not associated with any data (for example, a container). The component identifiers must be known to create such permissions, therefore access to the screen source code is required.

    In order to create a constraint, select the desired screen in the Screen drop-down list, specify the component path in the Component field, and click Add. Then set the access mode for the selected component in the Permissions panel.

    The rules to forming the component path are listed below:

    • If the component belongs to the screen, simply specify the component identifier, id.

    • If the component belongs to the frame that is embedded within the screen, specify the frame identifier, and then the component identifier separated by period.

    • If configuring permission for the TabSheet tab or the FieldGroup field, specify the component identifier, and then the tab or field identifier in square brackets.

    • To configure permission for an action, specify the component, holding the action, and then the action identifier in angle brackets. For example: customersTable<changeGrade>.

6.2.5. Access Groups

With access groups, users can be organized into a hierarchical structure and assigned constraints and arbitrary session attributes.

The user can be added to one group only, however the list of constraints and session attributes from all the groups up the hierarchy will be inherited.

User access groups can be managed from the Administration > Access Groups screen:

group users
6.2.5.1. Constraints

Constraints restrict user access to entity instances. Unlike the permissions which are applied to classes of entities, constraints are applied to particular entity instances that do not match the constraint conditions. Constraints can be set for creation, reading, updating and deletion. Besides, one can add custom constraints not related to CRUD actions.

A user gets the constraints list from all groups starting with their own one, and up the hierarchy. Thus, the following principle is implemented: the lower the users are in the groups hierarchy, the more constraints they have.

Note that constraints are checked for all operations performed from the client tier through the standard DataManager. If an entity does not match the constraints conditions during creation, modification or deletion, the RowLevelSecurityException is thrown. See also the Data Access Checks section for how security constraints are used by different mechanisms of the framework.

There are three types of constraint check: check in database, check in memory, check in database and in memory.

  1. For the constraints with check in database, conditions are specified using JPQL expression fragments. These fragments are appended to all entity instance selection queries. So the entities not matching the conditions are filtered on the database level. Constraints with database check can be used only for the read operation.

  2. For the constraints with check in memory, the conditions are specified using Groovy expressions. The expressions are executed for every entity in the checked graph of objects, and if the entity does not match the conditions, it is filtered from the graph.

  3. The constraints with check in database and in memory are combination of previous two types.

In order to create a constraint, open the Access Groups screen, select a group to create the constraint for, go to the Constraints tab and click Create:

constraint edit

Select an entity from the Entity Name drop-down list, operation type from the Operation Type drop-down list, check type from the Check type drop-down list. Depending on selected check type, you have to set JPQL conditions in the Join Clause and Where Clause fields and/or Groovy condition in the Groovy Script field. You can use the Constraint Wizard, which enables visual creation of the JPQL and Groovy conditions. When you select custom operation type, the required Code field appears, and you should set a specific code, which will be used to identify the constraint.

The JPQL editor in the Join Clause and Where Clause fields supports autocompletion for entity names and their attributes. In order to invoke autocompletion, press Ctrl+Space. If the invocation is made after the period symbol, an entity attributes list matching the context will be shown, otherwise – a list of all data model entities.

The following JPQL constraint rules apply:

  • The {E} string should be used as an alias of the entity being extracted. On execution of the query, it will be replaced with a real alias, specified in the query.

  • The following predefined constants can be used in JPQL parameters:

    • session$userLogin – login name of the current user (in case of substitution – the login name of the substituted user).

    • session$userId – ID of the current user (in case of substitution – ID of the substituted user).

    • session$userGroupId – group ID of the current user (in case of substitution − group ID of the substituted user).

    • session$XYZ – arbitrary attribute of the current user session, where XYZ is the attribute name.

  • The Where Clause field content is added to the where query clause using and condition. Adding where word is not needed, as it will be added automatically.

  • The Join Clause field content is added to the from query clause. It should begin with a comma, join or left join.

The simplest constraint example is shown on the figure above: users with this constraint will see only ref$Car entity instances that have VIN starting with '00'.

Another common example: if an entity refers to the User entity with many-to-many association, and you want the users to see only the instances they are referred to, you can use the member of JPQL operator in the Where Clause:

(select u from sec$User u where u.id = :session$userId) member of {E}.users

For an in-memory constraint, the userSession variable of the UserSession type is passed to the Groovy script. So you can use it to get attributes of the current user session, for example:

{E}.createdBy == userSession.user.login

A developer can check the constraints conditions for the particular entity using the following methods of the Security interface:

  • isPermitted(Entity, ConstraintOperationType) - to check constraints by the operation type.

  • isPermitted(Entity, String) - to check constraints by the string code.

Also, it is possible to link any any action based on the ItemTrackingAction class with a certain constraint. The constraintOperationType attribute should be set for the action XML element or using the setConstraintOperationType() method. Be aware that the constraint code will be executed on the client tier, so it must not use middleware classes.

Example:

<table>
    ...
    <actions>
        <action id="create"/>
        <action id="edit" constraintOperationType="update"/>
        <action id="remove" constraintOperationType="delete"/>
    </actions>
</table>

When a constraint is violated, a notification is shown to the user. Notification caption and message for each constraint can be localized: see Localization button on the Constraints tab of the Access Groups screen.

6.2.5.2. Session Attributes

The access group can determine the session attribute list for the users in this group. These attributes can be used when setting the constraints. The availability of the session attributes can be checked in the application code at the development stage, so the final system behavior for particular user groups can be controlled at the operation stage.

When logging in, all the attributes set for the user group and for all the groups up the hierarchy will be placed into the user session. If an attribute is found in several levels of the hierarchy, the uppermost group value will be used. Hence, overriding the attribute values at the lower levels of the hierarchy is not possible. In case of the override attempt, the WARN level message will be written to the server log.

In order to create an attribute in the Access Groups screen, select the group to create the attribute for, go to the Session Attributes tab, and click Create:

session attr edit

A unique attribute name, data type, and value must be specified.

A session attribute can be accessed in the application code in the following way:

@Inject
private UserSessionSource userSessionSource;
...
Integer accessLevel = userSessionSource.getUserSession().getAttribute("accessLevel");

A session attribute can be used in the constraints as a JPQL parameter by adding the session$ prefix:

{E}.accessLevel = :session$accessLevel

6.3. Data Access Checks

The following table explains how data access permissions and constraints are used by different mechanisms of the framework.

Entity Operations

Entity Attributes

Read Constraint
checked in database (1)

Read Constraint
checked in memory (2)

Create/Update/Delete
Constraints

EntityManager

No

No

No

No

No

DataManager on middle tier

No
Yes (3) (4)

No
Yes (5)

Yes

No
Yes (4)

No
Yes (4)

DataManager.secure on middle tier

DataManager on client tier

Yes (3)

No
Yes (5)

Yes

Yes

Yes

Generic UI data-aware components

Yes

Yes

- (6)

- (6)

- (6)

REST API /entities

Yes

Yes

Yes

Yes

Yes

REST API /queries

Yes

Yes

Yes

Yes

- (7)

REST API /services

Yes

Yes

- (8)

- (8)

- (8)

Notes:

1) Read constraint checked in database affects only the root entity.

// order is loaded only if it satisfies constraints on the Order entity
Order order = dataManager.load(Order.class).viewProperties("date", "amount", "customer.name").one();
// related customer is loaded regardless of database-checked constraints on Customer entity
assert order.getCustomer() != null;

2) Read constraint checked in memory affects the root entity and all linked entities in the loaded graph.

// order is loaded only if it satisfies constraints on the Order entity
Order order = dataManager.load(Order.class).viewProperties("date", "amount", "customer.name").one();
// related customer is not null only if it satisfies in-memory-checked constraints on Customer entity
if (order.getCustomer() != null) ...

3) Entity operation check in DataManager is performed for the root entity only.

// loading Order
Order order = dataManager.load(Order.class).viewProperties("date", "amount", "customer.name").one();
// related customer is loaded even if the user has no permission to read the Customer entity
assert order.getCustomer() != null;

4) DataManager checks entity operation permissions and in-memory constraints on middle tier only if you set cuba.dataManagerChecksSecurityOnMiddleware property to true.

5) DataManager checks entity attribute permissions only if you set cuba.entityAttributePermissionChecking to true.

6) UI components do not check constraints themselves, but when data is loaded through standard mechanisms, the constraints are applied by DataManager. As a result, if an entity instance is filtered out by constraints, the corresponding UI component is shown but it is empty. Also, it is possible to link any action based on the ItemTrackingAction class with a certain constraint, so the action is enabled only if the constraint check for the selected entity instance is successful.

7) REST queries are read-only.

8) REST service method parameters and results are not checked for compliance to access group constraints. The service behavior with respect to constraints is defined by how it loads and saves data, for example whether it uses DataManager or DataManager.secure().

6.4. Access Control Examples

This section provides some practical recommendations on how to configure data access for users.

6.4.1. Configuring Roles

The recommended way to configure roles and permissions is as follows:

  1. Create a Default role, which revokes all system rights. The simplest way to do it is to create a role of the Denying type. Select the Default role checkbox to automatically assign this role to all new users.

  2. Create a set of roles for granting specific rights to different user categories. There are two strategies for creating such roles:

    • Coarse-grained roles – each role has a permission set for the full range of user responsibilities in the system. For example, Sales Manager, Accountant. Only one role is assigned to each user when using this strategy, excluding the Default role.

    • Fine-grained roles – each role has a small permission set to execute specific functions within the system. For example, Task Creator, References Editor. Each user will then be assigned numerous roles according to their range of responsibilities.

    The strategies can also be combined. Create a set of roles for granting specific rights to different user categories. There are two strategies for creating such roles:

  3. It is possible to leave the system administrator without any assigned roles, in which case, they will have all the rights to all the system objects. Alternatively, a Super type role, overriding any restriction imposed by other roles, can be assigned.

Access to administrative functionality

Below is a quick reference of permissions that should be allowed in a Denying role to provide access to the Administration functionality. For example, if you want to allow nothing but Entity log functionality, set the permissions mentioned in the corresponding section.

It is recommended to provide at least a read-only permissions for the sys$FileDescriptor entity as it is widely used by the platform: emailing, attachments, logging etc.

The permissions described below can be configured in the Role editor on the corresponding tabs: Entity, Screen or Specific.

Moreover, the default access to system entities can be configured using the cuba.defaultPermissionValuesConfig application property.

Users

The User entity may be used as a reference attribute in your data model. To make it visible in lookup fields and drop-down lists, it will be enough to set the permission for the sec$User entity.

In case you want to create and edit the User entity from a Denying role, the following set of permissions is required:

  • Entities: sec$User, sec$Group; (optionally) sec$Role, sec$UserRole, sec$UserSubstitution.

Permission to read the sec$UserSubstitution entity is essential for functioning of the user substitution mechanism.

  • Screens: Users menu item, sec$User.edit, sec$Group.lookup; (optionally) sec$Group.edit, sec$Role.edit, sec$Role.lookup, sec$User.changePassword, sec$User.copySettings, sec$User.newPasswords, sec$User.resetPasswords, sec$UserSubstitution.edit.

Access Groups

Creating and managing the user access groups and security constraints.

  • Entities: sec$Group, sec$Constraint, sec$SessionAttribute, sec$LocalizedConstraintMessage.

  • Screens: Access Groups menu item, sec$Group.lookup, sec$Group.edit, sec$Constraint.edit, sec$SessionAttribute.edit, sec$LocalizedConstraintMessage.edit.

Dynamic Attributes

Access to additional non-persistent entity attributes.

  • Entities: sys$Category, sys$CategoryAttribute, and the required entities of your data model.

  • Screens: Dynamic Attributes menu item, sys$Category.edit, sys$CategoryAttribute.edit, dynamicAttributesConditionEditor, dynamicAttributesConditionFrame.

User Sessions

Viewing the user sessions data.

  • Entities: sec$User, sec$UserSessionEntity.

  • Screens: User Sessions menu item, sessionMessageWindow.

Locks

Setting up Pessimistic locking for the entities.

  • Entities: sys$LockInfo, sys$LockDescriptor, and the required entities of your data model.

  • Screens: Locks menu item, sys$LockDescriptor.edit.

External Files

Access to the application File storage.

  • Entities: sys$FileDescriptor.

  • Screens: External Files menu item; (optionally) sys$FileDescriptor.edit.

Scheduled Tasks

Creating and managing scheduled tasks.

  • Entities: sys$ScheduledTask, sys$ScheduledExecution.

  • Screens: Scheduled Tasks menu item, sys$ScheduledExecution.browse, sys$ScheduledTask.edit.

Entity Inspector

Working with any application objects from the screens dynamically generated by the entity inspector.

  • Entities: the required entities of your data model.

  • Screens: Entity Inspector menu item, entityInspector.edit, and the required entities of your data model.

Entity Log

Tracking the entity persistence at the entity listeners level.

  • Entities: sec$EntityLog, sec$User, sec$EntityLogAttr, sec$LoggedAttribute, sec$LoggedEntity, and the required entities of your data model.

  • Screens: Entity Log menu item.

User Session Log

Viewing the historical data on the users' login and logout, or user sessions.

  • Entities: sec$SessionLogEntry.

  • Screens: User Session Log menu item.

Email History

Viewing the emails sent from the application.

  • Entities: sys$SendingMessage, sys$SendingAttachment, sys$FileDescriptor (for attachments).

  • Screens: Email History menu item, sys$SendingMessage.attachments.

Server Log

Viewing and downloading the application log files.

  • Entities: sys$FileDescriptor.

  • Screens: Server Log menu item, serverLogDownloadOptionsDialog.

  • Specific: Download log files.

Screen Profiler

The statistics on the application screens usage and spent time.

  • Entities: sec$User, sys$ScreenProfilerEvent.

  • Screens: Screen Profiler menu item.

Reports

Running reports, see Report Generator add-on.

  • Entities: report$Report, report$ReportInputParameter, report$ReportGroup.

  • Screens: report$inputParameters, commonLookup, report$Report.run, report$showChart (if contains chart templates).

6.4.2. Creating Local Administrators

The hierarchical structure of access groups combined with the constraints inheritance enables creating local administrators, by delegating creation and configuration of users and their rights under organization departments.

The local administrators have access to the security subsystem screens; however, they only see the users and groups in their access group and below. Local administrators can create subgroups and users and assign roles available in the system, however, they will have at least the same constraints as the administrator who created them.

The global administrator in the root access group should create the roles that will be available to the local administrators for assigning to the users. The local administrators should not be able to create and update the roles.

An example access group structure is presented below:

local admins groups

Problem:

  • The users under the Departments group should only see the users of their own group and the groups below.

  • Each subgroup – Dept 1, Dept 2, etc. should have its own administrator, who can create users and assign them the available roles.

Solution:

  • Add the following constraints for the Departments group:

    local admins constraints
    • For the sec$Group entity:

      {E}.id in (
        select h.group.id from sec$GroupHierarchy h
        where h.group.id = :session$userGroupId or h.parent.id = :session$userGroupId
      )

      With this constraint, the users will not be able to see the groups higher than their own.

    • For the sec$User entity:

      {E}.group.id in (
        select h.group.id from sec$GroupHierarchy h
        where h.group.id = :session$userGroupId or h.parent.id = :session$userGroupId
      )

      With this constraint, the users will not be able to see the users in groups higher than their own.

    • For the sec$Role entity:

      ({E}.description is null or {E}.description not like '[hide]')

      With this constraint, the users will not be able to view the roles that have the [hide] string in the description attribute.

  • Create a role that denies editing roles and permissions:

    local admins role
    • Select the Default role checkbox:

    • Add the [hide] string to the Description field.

    • In the Entities tab, deny create, update and delete operations for the sec$Role and sec$Permission entities (to add permissions for the sec$Permission object, select the System level checkbox).

    All created users, including the local administrators, will get the local_user role. This role is invisible to the users in the Departments group, so even the local administrators are unable to unassign this role from themselves. Local administrators can only operate on the existing roles that have been created for them by the global administrator. Obviously, the roles available to department users should not remove restrictions imposed by the local_user role.

6.5. Integration with LDAP

CUBA applications can be integrated with LDAP to provide the following benefits:

  1. Storing user passwords centrally in the LDAP database.

  2. For Windows domain users, ability to log in using Single Sign-On without having to specify the username and password.

If the LDAP integration is enabled, a user still needs an account in the application. All the user permissions and properties (except password) are stored in the application database, LDAP is used only for authentication. It is recommended to leave the application password empty for most users except the ones that require the standard authentication (see below). The password field in the user editor screen is not required if the cuba.web.requirePasswordForNewUsers property is set to false.

If the user login is listed in the cuba.web.standardAuthenticationUsers application property, the application tries to authenticate the user only by the password hash stored in the database. As a result, a user from this list can log in to the system with this password if he is not registered in LDAP.

A CUBA-based application interacts with LDAP via the LdapLoginProvider bean.

You can use the Jespa library with the corresponding LoginProvider described in the Active Directory Integration Using Jespa section in order to enable advanced integration with Active Directory, including Single Sign-On for Windows domain users.

You can implement your own login mechanism using custom LoginProvider, HttpRequestFilter or events described in Web Login Specifics.

Also, you can enable LDAP authentication for REST API clients: REST API Authentication with LDAP.

6.5.1. Basic LDAP Integration

If the cuba.web.ldap.enabled property is set to true, the LdapLoginProvider is enabled. In this case, the Spring LDAP library is used for user authentication.

The following Web Client application properties are used to set up LDAP integration:

Example of local.app.properties file for the Web Client block:

cuba.web.ldap.enabled = true
cuba.web.ldap.urls = ldap://192.168.1.1:389
cuba.web.ldap.base = ou=Employees,dc=mycompany,dc=com
cuba.web.ldap.user = cn=System User,ou=Employees,dc=mycompany,dc=com
cuba.web.ldap.password = system_user_password

In case of the integration with Active Directory, when creating users in the application, specify their sAMAccountName without domain as a login.

6.5.2. Active Directory Integration Using Jespa

Jespa is a Java library that enables integration of Active Directory service and Java applications using NTLMv2. For details, see http://www.ioplex.com.

6.5.2.1. Including the Library

Download the library at http://www.ioplex.com and place the JAR in a repository registered in your build.gradle script. This can be mavenLocal() or an in-house repository.

Add the following dependencies to the web module configuration section in build.gradle:

configure(webModule) {
    ...
    dependencies {
        compile('com.company.thirdparty:jespa:1.1.17')  // from a custom repository
        compile('jcifs:jcifs:1.3.17')                   // from Maven Central
        ...

Create a LoginProvider implementation class in the web module:

package com.company.sample.web;

import com.company.sample.config.ActiveDirectoryConfig;
import com.company.sample.web.sys.DomainAliasesResolver;
import com.google.common.collect.ImmutableMap;
import com.haulmont.cuba.core.global.ClientType;
import com.haulmont.cuba.core.global.GlobalConfig;
import com.haulmont.cuba.core.sys.AppContext;
import com.haulmont.cuba.core.sys.ConditionalOnAppProperty;
import com.haulmont.cuba.security.auth.*;
import com.haulmont.cuba.security.global.LoginException;
import com.haulmont.cuba.web.auth.WebAuthConfig;
import com.haulmont.cuba.web.security.LoginProvider;
import jespa.http.HttpSecurityService;
import jespa.ntlm.NtlmSecurityProvider;
import jespa.security.PasswordCredential;
import jespa.security.SecurityProviderException;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;

import javax.annotation.Nullable;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;

import static com.haulmont.cuba.web.security.ExternalUserCredentials.EXTERNAL_AUTH_USER_SESSION_ATTRIBUTE;

@ConditionalOnAppProperty(property = "activeDirectory.integrationEnabled", value = "true")
@Component("sample_JespaAuthProvider")
public class JespaAuthProvider extends HttpSecurityService implements LoginProvider, Ordered {

    private static final Logger log = LoggerFactory.getLogger(JespaAuthProvider.class);

    @Inject
    private GlobalConfig globalConfig;
    @Inject
    private WebAuthConfig webAuthConfig;
    @Inject
    private DomainAliasesResolver domainAliasesResolver;
    @Inject
    private AuthenticationService authenticationService;

    private static Map<String, DomainInfo> domains = new HashMap<>();
    private static String defaultDomain;

    @PostConstruct
    public void init() throws ServletException {
        initDomains();

        Map<String, String> properties = new HashMap<>();
        properties.put("jespa.bindstr", getBindStr());
        properties.put("jespa.service.acctname", getAcctName());
        properties.put("jespa.service.password", getAcctPassword());
        properties.put("jespa.account.canonicalForm", "3");
        properties.put("jespa.log.path", globalConfig.getLogDir() + "/jespa.log");
        properties.put("http.parameter.anonymous.name", "anon");
        fillFromSystemProperties(properties);

        try {
            super.init(JespaAuthProvider.class.getName(), null, properties);
        } catch (SecurityProviderException e) {
            throw new ServletException(e);
        }
    }

    @Nullable
    @Override
    public AuthenticationDetails login(Credentials credentials) throws LoginException {
        LoginPasswordCredentials lpCredentials = (LoginPasswordCredentials) credentials;

        String login = lpCredentials.getLogin();
        // parse domain by login
        String domain;
        int atSignPos = login.indexOf("@");
        if (atSignPos >= 0) {
            String domainAlias = login.substring(atSignPos + 1);
            domain = domainAliasesResolver.getDomainName(domainAlias).toUpperCase();
        } else {
            int slashPos = login.indexOf('\\');
            if (slashPos <= 0) {
                throw new LoginException("Invalid name: %s", login);
            }
            String domainAlias = login.substring(0, slashPos);
            domain = domainAliasesResolver.getDomainName(domainAlias).toUpperCase();
        }

        DomainInfo domainInfo = domains.get(domain);
        if (domainInfo == null) {
            throw new LoginException("Unknown domain: %s", domain);
        }

        Map<String, String> securityProviderProps = new HashMap<>();
        securityProviderProps.put("bindstr", domainInfo.getBindStr());
        securityProviderProps.put("service.acctname", domainInfo.getAcctName());
        securityProviderProps.put("service.password", domainInfo.getAcctPassword());
        securityProviderProps.put("account.canonicalForm", "3");
        fillFromSystemProperties(securityProviderProps);

        NtlmSecurityProvider provider = new NtlmSecurityProvider(securityProviderProps);
        try {
            PasswordCredential credential = new PasswordCredential(login, lpCredentials.getPassword().toCharArray());
            provider.authenticate(credential);
        } catch (SecurityProviderException e) {
            throw new LoginException("Authentication error: %s", e.getMessage());
        }

        TrustedClientCredentials trustedCredentials = new TrustedClientCredentials(
                lpCredentials.getLogin(),
                webAuthConfig.getTrustedClientPassword(),
                lpCredentials.getLocale(),
                lpCredentials.getParams());

        trustedCredentials.setClientInfo(lpCredentials.getClientInfo());
        trustedCredentials.setClientType(ClientType.WEB);
        trustedCredentials.setIpAddress(lpCredentials.getIpAddress());
        trustedCredentials.setOverrideLocale(lpCredentials.isOverrideLocale());
        trustedCredentials.setSyncNewUserSessionReplication(lpCredentials.isSyncNewUserSessionReplication());

        Map<String, Serializable> targetSessionAttributes;
        Map<String, Serializable> sessionAttributes = lpCredentials.getSessionAttributes();
        if (sessionAttributes != null
                && !sessionAttributes.isEmpty()) {
            targetSessionAttributes = new HashMap<>(sessionAttributes);
            targetSessionAttributes.put(EXTERNAL_AUTH_USER_SESSION_ATTRIBUTE, true);
        } else {
            targetSessionAttributes = ImmutableMap.of(EXTERNAL_AUTH_USER_SESSION_ATTRIBUTE, true);
        }
        trustedCredentials.setSessionAttributes(targetSessionAttributes);

        return authenticationService.login(trustedCredentials);
    }

    @Override
    public boolean supports(Class<?> credentialsClass) {
        return LoginPasswordCredentials.class.isAssignableFrom(credentialsClass);
    }

    @Override
    public int getOrder() {
        return HIGHEST_PLATFORM_PRECEDENCE + 50;
    }

    @Override
    public void destroy() {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        if (httpServletRequest.getHeader("User-Agent") != null) {
            String ua = httpServletRequest.getHeader("User-Agent")
                    .toLowerCase();

            boolean windows = ua.contains("windows");
            boolean gecko = ua.contains("gecko") && !ua.contains("webkit");

            if (!windows && gecko) {
                chain.doFilter(request, response);
                return;
            }
        }
        super.doFilter(request, response, chain);
    }

    private void initDomains() {
        String domainsStr = AppContext.getProperty("activeDirectory.domains");
        if (StringUtils.isEmpty(domainsStr)) {
            return;
        }

        String[] strings = domainsStr.split(";");
        for (int i = 0; i < strings.length; i++) {
            String domain = strings[i];
            domain = domain.trim();

            if (StringUtils.isEmpty(domain)) {
                continue;
            }

            String[] parts = domain.split("\\|");
            if (parts.length != 4) {
                log.error("Invalid ActiveDirectory domain definition: " + domain);
                break;
            } else {
                domains.put(parts[0], new DomainInfo(parts[1], parts[2], parts[3]));
                if (i == 0) {
                    defaultDomain = parts[0];
                }
            }
        }
    }

    public String getDefaultDomain() {
        return defaultDomain != null ? defaultDomain : "";
    }

    public String getBindStr() {
        return getBindStr(getDefaultDomain());
    }

    public String getBindStr(String domain) {
        initDomains();
        DomainInfo domainInfo = domains.get(domain);
        return domainInfo != null ? domainInfo.getBindStr() : "";
    }

    public String getAcctName() {
        return getAcctName(getDefaultDomain());
    }

    public String getAcctName(String domain) {
        initDomains();
        DomainInfo domainInfo = domains.get(domain);
        return domainInfo != null ? domainInfo.getAcctName() : "";
    }

    public String getAcctPassword() {
        return getAcctPassword(getDefaultDomain());
    }

    public String getAcctPassword(String domain) {
        initDomains();
        DomainInfo domainInfo = domains.get(domain);
        return domainInfo != null ? domainInfo.getAcctPassword() : "";
    }

    public void fillFromSystemProperties(Map<String, String> params) {
        for (String name : AppContext.getPropertyNames()) {
            if (name.startsWith("jespa.")) {
                params.put(name, AppContext.getProperty(name));
            }
        }
    }

    public static class DomainInfo {

        private final String bindStr;
        private final String acctName;
        private final String acctPassword;

        DomainInfo(String bindStr, String acctName, String acctPassword) {
            this.acctName = acctName;
            this.acctPassword = acctPassword;
            this.bindStr = bindStr;
        }

        public String getBindStr() {
            return bindStr;
        }

        public String getAcctName() {
            return acctName;
        }

        public String getAcctPassword() {
            return acctPassword;
        }
    }
}

Create a bean intended for resolving domains by their aliases in the web module:

package com.company.sample.web;

import com.haulmont.cuba.core.sys.AppContext;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Component(DomainAliasesResolver.NAME)
public class DomainAliasesResolver {

    public static final String NAME = "sample_DomainAliasesResolver";

    private static final Logger log = LoggerFactory.getLogger(DomainAliasesResolver.class);

    private Map<String, String> aliases = new HashMap<>();

    public DomainAliasesResolver() {
        String domainAliases = AppContext.getProperty("activeDirectory.aliases");
        if (StringUtils.isEmpty(domainAliases)) {
            return;
        }

        List<String> aliasesPairs = Arrays.stream(StringUtils.split(domainAliases, ';'))
                .filter(StringUtils::isNotEmpty)
                .collect(Collectors.toList());

        for (String aliasDefinition : aliasesPairs) {
            String[] aliasParts = StringUtils.split(aliasDefinition, '|');
            if (aliasParts == null
                    || aliasParts.length != 2
                    || StringUtils.isBlank(aliasParts[0])
                    || StringUtils.isBlank(aliasParts[1])) {
                log.warn("Incorrect domain alias definition: '{}'", aliasDefinition);
            } else {
                aliases.put(aliasParts[0].toLowerCase(), aliasParts[1]);
            }
        }
    }

    public String getDomainName(String alias) {
        String alias_lc = alias.toLowerCase();

        String domain = aliases.get(alias_lc);
        if (domain == null) {
            return alias;
        }

        log.debug("Resolved domain '{}' from alias '{}'", domain, alias);

        return domain;
    }
}
6.5.2.2. Setting Up Configuration
  • Follow the steps described in InstallationStep 1: Create the Computer Account for NETLOGON Communication of the Jespa Operator’s Manual, which is available at http://www.ioplex.com/support.html.

  • Set domain parameters in the activeDirectory.domains property in the local.app.properties file. Each domain descriptor should have the following format: domain_name|full_domain_name|service_account_name|service_account_password. Domain descriptors are separated by semicolons.

    Example:

    activeDirectory.domains = MYCOMPANY|mycompany.com|JESPA$@MYCOMPANY.COM|password1;TEST|test.com|JESPA$@TEST.COM|password2
  • Enable the Active Directory integration by setting the activeDirectory.integrationEnabled property in the local.app.properties file:

    activeDirectory.integrationEnabled = true
  • Configure additional Jespa properties in the local.app.properties file (see Jespa Operator’s Manual). For example:

    jespa.log.level=3

    If the application is deployed to Tomcat, Jespa log file can be found in tomcat/logs.

  • Add the server address to the local intranet in the browser settings:

    • For Internet Explorer and Chrome: Settings > Security > Local intranet > Sites > Advanced

    • For Firefox: about:config > network.automatic-ntlm-auth.trusted-uris=http://myapp.mycompany.com

6.6. Single-Sign-On for CUBA Applications

Single-sign-on (SSO) for CUBA applications allows a user to log in to the multiple running applications by entering a single login name and password once in a browser session.

The IDP addon is designed to simplify setting up SSO in CUBA applications. See the addon documentation for more details.

6.7. Social Login

Social login is a form of single sign-on that allows you to use credentials from a social networking site such as Facebook, Twitter or Google+, to sign in to a CUBA application instead of creating a new login account explicitly for the application.

See also the Anonymous Access and Social Login guide.

The guide includes an example of setting up public access to the CUBA application, as well as the implementation of custom login to the application using a Google, Facebook, or GitHub account.

In the following example, we will consider the social login using Facebook. Facebook uses OAuth2 authorization mechanism, for more details consult the documentation on Facebook API and Facebook Login Flow: https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow.

The source code of this sample project is available on GitHub, below are the key points of the social login implementation.

  1. In order to connect your application to Facebook, you will need to create the App ID (unique application identifier) and App Secret (a kind of password that authenticates requests from your application to Facebook servers). Follow the instruction and register the generated values in app.properties file of the core module as the facebook.appId and facebook.appSecret application properties respectively, for example:

    facebook.appId = 123456789101112
    facebook.appSecret = 123456789101112abcde131415fghi16

    Also register the URL that you used for the Facebook app registration in the cuba.webAppUrl property in the core and web application properties files, for example:

    cuba.webAppUrl = http://cuba-fb.test:8080/app
  2. Extend the login screen and add the button for social login. This button will invoke the loginFacebook() method - the entry point to the social login flow.

  3. To use Facebook user accounts, we need to add one extra field to the standard CUBA user account. Extend the User entity and add the facebookId attribute of String type to it:

    @Column(name = "FACEBOOK_ID")
    protected String facebookId;
  4. Create a service that will look for a user with a given facebookId in the application database and either return the existent user or create a new one:

    public interface SocialRegistrationService {
        String NAME = "demo_SocialRegistrationService";
    
        User findOrRegisterUser(String facebookId, String email, String name);
    }
    @Service(SocialRegistrationService.NAME)
    public class SocialRegistrationServiceBean implements SocialRegistrationService {
        @Inject
        private Metadata metadata;
    
        @Inject
        private Persistence persistence;
    
        @Inject
        private Configuration configuration;
    
        @Override
        @Transactional
        public User findOrRegisterUser(String facebookId, String email, String name) {
            EntityManager em = persistence.getEntityManager();
    
            TypedQuery<SocialUser> query = em.createQuery("select u from sec$User u where u.facebookId = :facebookId",
                    SocialUser.class);
            query.setParameter("facebookId", facebookId);
            query.setViewName(View.LOCAL);
    
            SocialUser existingUser = query.getFirstResult();
            if (existingUser != null) {
                return existingUser;
            }
    
            SocialRegistrationConfig config = configuration.getConfig(SocialRegistrationConfig.class);
    
            Group defaultGroup = em.find(Group.class, config.getDefaultGroupId(), View.MINIMAL);
    
            SocialUser user = metadata.create(SocialUser.class);
            user.setFacebookId(facebookId);
            user.setEmail(email);
            user.setName(name);
            user.setGroup(defaultGroup);
            user.setActive(true);
            user.setLogin(email);
    
            em.persist(user);
    
            return user;
        }
    }
  5. Create a service to manage the login process. In this example it is FacebookService that contains two methods: getLoginUrl() and getUserData().

    • getLoginUrl() will generate the login URL based on the application URL and OAuth2 response type (code, access token or both; see more on response types in Facebook API documentation). The complete implementation of this method you can find in the FacebookServiceBean.java file.

    • getUserData() will look for the Facebook user by the given application URL and code, and either return the personal data of the existent user or create a new one. In this example we want to get the user’s id, name and email, the id will correspond the facebookId attribute created above.

  6. Define the facebook.fields and facebook.scope application properties in the app.properties file of the core module:

    facebook.fields = id,name,email
    facebook.scope = email
  7. Go back to the loginFacebook() method in the controller of the extended login window. The full code of the controller you can find in the ExtAppLoginWindow.java file.

    In this method we add the request handler to the current session, save the current URL and invoke the redirect to the Facebook authentication form in the browser:

    private RequestHandler facebookCallBackRequestHandler =
            this::handleFacebookCallBackRequest;
    
    private URI redirectUri;
    
    @Inject
    private FacebookService facebookService;
    
    @Inject
    private GlobalConfig globalConfig;
    
    public void loginFacebook() {
        VaadinSession.getCurrent()
            .addRequestHandler(facebookCallBackRequestHandler);
    
        this.redirectUri = Page.getCurrent().getLocation();
    
        String loginUrl = facebookService.getLoginUrl(globalConfig.getWebAppUrl(), OAuth2ResponseType.CODE);
        Page.getCurrent()
            .setLocation(loginUrl);
    }

    The handleFacebookCallBackRequest() method will handle the callback after the Facebook authentication form. Firstly, we use the UIAccessor instance to lock the UI until the login request is proceeded.

    Then, FacebookService will get the Facebook user account email and id. After that, the corresponding CUBA user will be found by facebookId or registered on the fly in the system.

    Next, the authentication is triggered, the user session on behalf of this user is loaded, and the UI is updated. After that, we remove the Facebook callback handler, as far as we no longer expect authentication.

    public boolean handleFacebookCallBackRequest(VaadinSession session, VaadinRequest request,
                                                VaadinResponse response) throws IOException {
        if (request.getParameter("code") != null) {
            uiAccessor.accessSynchronously(() -> {
                try {
                    String code = request.getParameter("code");
    
                    FacebookUserData userData = facebookService.getUserData(globalConfig.getWebAppUrl(), code);
    
                    User user = socialRegistrationService.findOrRegisterUser(
                            userData.getId(), userData.getEmail(), userData.getName());
    
                    App app = App.getInstance();
                    Connection connection = app.getConnection();
                    Locale defaultLocale = messages.getTools().getDefaultLocale();
    
                    connection.login(new ExternalUserCredentials(user.getLogin(), defaultLocale));
                } catch (Exception e) {
                    log.error("Unable to login using Facebook", e);
                } finally {
                     session.removeRequestHandler(facebookCallBackRequestHandler);
                }
            });
    
            ((VaadinServletResponse) response).getHttpServletResponse().
                sendRedirect(ControllerUtils.getLocationWithoutParams(redirectUri));
    
            return true;
        }
    
        return false;
    }

Now, when a user clicks the Facebook button on the login screen, the application will ask them for permission to use their Facebook profile and email, and if this permission is granted, the logged in user will be redirected to the main application page.

You can implement your own login mechanism using custom LoginProvider, HttpRequestFilter or events described in the Web Login section.

Appendix A: Configuration Files

This appendix describes the main configuration files included in CUBA-applications.

A.1. app-component.xml

app-component.xml file is required for using the current application as a component of another application. The file defines the dependencies on other components, describes the existing application modules, generated artifacts and exposed application properties.

The app-component.xml file should be located in a package, specified in the App-Component-Id entry of the global module JAR manifest. This manifest entry allows the build system to find components for a project in the build class path. As a result, in order to use some component in your project, just define the component’s global artifact coordinates in the dependencies/appComponent items of your build.gradle.

By convention, the app-component.xml is located in the root package of the project (defined in metadata.xml) which is also equal to the projects’s artifact group (defined in build.gradle):

App-Component-Id == root-package == cuba.artifact.group == e.g. 'com.company.sample'

Use CUBA Studio to generate the app-component.xml descriptor and the manifest entries for the current project automatically.

Using 3rd party dependencies as appJars:

If you want the component’s 3rd party dependencies to be deployed with your application module’s artifacts (e.g. app-comp-core or app-comp-web) into the tomcat/webapps/app[-core]/WEB-INF/lib/ folder, you should add these dependencies as appJar libraries:

<module blocks="core"
        dependsOn="global,jm"
        name="core">
    <artifact appJar="true"
              name="cuba-jm-core"/>
    <artifact classifier="db"
              configuration="dbscripts"
              ext="zip"
              name="cuba-jm-core"/>
    <!-- Specify only the artifact name for your appJar 3rd party library -->
    <artifact name="javamelody-core"
              appJar="true"
              library="true"/>
</module>

If you don’t want to use a project as an app component, you should add such dependencies as appJars to the deploy task of your build.gradle:

configure(coreModule) {
    //...
    task deploy(dependsOn: assemble, type: CubaDeployment) {
        appName = 'app-core'
        appJars('app-global', 'app-core', 'javamelody-core')
    }
    //...
}

A.2. context.xml

context.xml file is the application deployment descriptor for the Apache Tomcat server. In a deployed application, this file is located in the META-INF folder of the web application directory or the WAR file, for example, tomcat/webapps/app-core/META-INF/context.xml. In an application project, this file can be found in the /web/META-INF folders of the core, web and portal modules.

The main purpose of this file in the Middleware block is to define a JDBC data source with the JNDI name, specified in the cuba.dataSourceJndiName application property.

An example of a data source declaration for PostgreSQL:

<Resource
  name="jdbc/CubaDS"
  type="javax.sql.DataSource"
  maxIdle="2"
  maxTotal="20"
  maxWaitMillis="5000"
  driverClassName="org.postgresql.Driver"
  username="cuba"
  password="cuba"
  url="jdbc:postgresql://localhost/sales"/>

An example of a data source declaration for Microsoft SQL Server 2005:

<Resource
  name="jdbc/CubaDS"
  type="javax.sql.DataSource"
  maxIdle="2"
  maxTotal="20"
  maxWaitMillis="5000"
  driverClassName="net.sourceforge.jtds.jdbc.Driver"
  username="sa"
  password="saPass1"
  url="jdbc:jtds:sqlserver://localhost/sales"/>

An example of a data source declaration for Microsoft SQL Server 2008+:

<Resource
  name="jdbc/CubaDS"
  type="javax.sql.DataSource"
  maxIdle="2"
  maxTotal="20"
  maxWaitMillis="5000"
  driverClassName="com.microsoft.sqlserver.jdbc.SQLServerDriver"
  username="sa"
  password="saPass1"
  url="jdbc:sqlserver://localhost;databaseName=sales"/>

An example of a data source declaration for Oracle:

<Resource
  name="jdbc/CubaDS"
  type="javax.sql.DataSource"
  maxIdle="2"
  maxTotal="20"
  maxWaitMillis="5000"
  driverClassName="oracle.jdbc.OracleDriver"
  username="sales"
  password="sales"
  url="jdbc:oracle:thin:@//localhost:1521/orcl"/>

An example of a data source declaration for MySQL:

<Resource
  type="javax.sql.DataSource"
  name="jdbc/CubaDS"
  maxIdle="2"
  maxTotal="20"
  maxWaitMillis="5000"
  driverClassName="com.mysql.jdbc.Driver"
  password="cuba"
  username="cuba"
  url="jdbc:mysql://localhost/sales?useSSL=false&amp;allowMultiQueries=true"/>

The following line disables the serialization of HTTP sessions:

<Manager pathname=""/>

A.3. default-permission-values.xml

The files of this type are used to define the user’s default permissions. Default permission values are used when no role defines an explicit value for permission target. It is necessary mostly for denying roles: without this file the user with a denying role by default doesn’t have access to mainWindow screen and to filter screens.

Such files are not generated automatically by Studio, they should be created manually in the core module.

The file location is specified in the [cuba.defaultPermissionValuesConfig] application property. If this property is not defined in the application, the default cuba-default-permission-values.xml file will be used.

The file has the following structure:

default-permission-values - the root element, which has only one nested element - permission.

permission - the permission itself: it determines the object type and the permission imposed on it.

permission has three attributes:

  • target - permission object: determines the specific object the permission is imposed on. The format of the attribute depends on the permission type: for screens - the id of the screen, for entity operations - the entity id with the operation type, for example, target="sec$Filter:read", and so on.

  • value - permission value. Can be 0 or 1 (denied or allowed, respectively).

  • type - the type of permission object:

    • 10 - screen,

    • 20 - entity operation,

    • 30 - entity attribute,

    • 40 - application-specific permission,

    • 50 - UI component.

For example:

<?xml version="1.0" encoding="UTF-8"?>
<default-permission-values xmlns="http://schemas.haulmont.com/cuba/default-permission-values.xsd">
    <permission target="dynamicAttributesConditionEditor" value="0" type="10"/>
    <permission target="dynamicAttributesConditionFrame" value="0" type="10"/>
    <permission target="sec$Filter:read" value="1" type="20"/>
    <permission target="cuba.gui.loginToClient" value="1" type="40"/>
</default-permission-values>

A.4. dispatcher-spring.xml

The files of this type define configuration of an additional Spring Framework container for client blocks containing Spring MVC controllers.

The additional container for controllers is created with the main container (configured by spring.xml files) as its parent. Therefore, the beans of the controllers container can use the beans of the main container, while the beans of the main container cannot "see" the beans of the controllers container.

The dispatcher-spring.xml file of the project is specified in the cuba.dispatcherSpringContextConfig application property.

The platform web and portal modules already contain such configuration files: cuba-dispatcher-spring.xml and cuba-portal-dispatcher-spring.xml respectively.

If you have created Spring MVC controllers in your project (for example, in the web module), add the following configuration:

  • Assuming that your controllers are located inside the com.company.sample.web.controller package, create the modules/web/src/com/company/sample/web/dispatcher-config.xml file with the following content:

    <?xml version="1.0" encoding="UTF-8"?>
    <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.2.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd">
    
        <context:annotation-config/>
    
        <context:component-scan base-package="com.company.sample.web.controller"/>
    
    </beans>
  • Include the file into the cuba.dispatcherSpringContextConfig application property in the web-app.properties file:

    cuba.dispatcherSpringContextConfig = +com/company/sample/web/dispatcher-config.xml

Controllers defined in the web module will be available on addresses starting with the URL of the dispatcher servlet, which is /dispatch by default. For example:

http://localhost:8080/app/dispatch/my-controller-endpoint

Controllers defined in the portal module will be available in the root context of the web application. For example:

http://localhost:8080/app-portal/my-controller-endpoint

Files of this type are used in the Web Client to define the structure of the application main menu.

The file location is specified in the cuba.menuConfig application property. When you create a new project in Studio, it creates the web-menu.xml file in the root package of the web module, for example modules/web/src/com/company/sample/web-menu.xml.

menu-config is the root XML element. Elements of menu-config form a tree structure where menu elements are branches and item and separator elements are leaves.

  • menu element attributes:

    • id – unique identifier of the element. The localized caption of the menu element is determined by the id (see below).

    • description - a text shown as a tooltip on mouse hover. You can use localized messages from the main message pack.

    • icon - icon of the menu element. See icon for details.

    • insertBefore, insertAfter – determines whether the item should be inserted before or after a particular element or a menu item with the specified identifier. This attribute is used to insert an element to an appropriate place in the menu defined in the files of application components. Before and after elements cannot be used at the same time.

    • stylename - defines a style name for the menu item. See Themes for details.

  • item element attributes:

    • id – unique identifier of the element. The localized caption of the menu element is determined by the id (see below). If no screen, bean, class attributes are defined, the id is used to point to a screen with the same id. When the user clicks on the menu item, the corresponding screen will be opened in the main application window.

      <item id="sample_Foo.browse"/>
    • screen - a screen identifier. It can be used to include one screen into the menu multiple times. When the user clicks on the menu item, the corresponding screen will be opened in the main application window.

      <item id="foo1" screen="sample_Foo.browse"/>
      <item id="foo2" screen="sample_Foo.browse"/>
    • bean - a bean name. Must be used together with beanMethod. When the user clicks on the menu item, the method of the bean is invoked.

      <item bean="sample_FooProcessor" beanMethod="processFoo"/>
    • class - fully qualified name of a class which extends Runnable. When the user clicks on the menu item, an instance of the specified class is created and its run() method is invoked.

      <item class="com.company.sample.web.FooProcessor"/>
    • description - a text which is shown as a tooltip on mouse hover. You can also use localized messages from the main message pack.

      <item id="sample_Foo.browse" description="mainMsg://fooBrowseDescription"/>
    • shortcut – a keyboard shortcut for this menu item. Possible modifiers – ALT, CTRL, SHIFT – are separated with “-”. For example:

      shortcut="ALT-C"
      shortcut="ALT-CTRL-C"
      shortcut="ALT-CTRL-SHIFT-C"

      Shortcuts can also be configured in application properties and then used in menu.xml file in the following way:

      shortcut="${sales.menu.customer}"
    • openType – screen open mode, corresponds to the OpenMode enum: NEW_TAB, THIS_TAB, DIALOG. Default value is NEW_TAB.

    • icon - icon of the menu element. See icon for details.

    • insertBefore, insertAfter – determines whether the item should be inserted before or after a particular element or a menu item with the specified identifier.

    • resizable – only relevant to the DIALOG screen open mode. Controls window resizing ability. Possible values − true, false. By default, the main menu does not affect the ability to resize dialog windows.

    • stylename - defines a style name for the menu item. See Themes for details.

  • item sub-elements:

Example of a menu file:

<menu-config xmlns="http://schemas.haulmont.com/cuba/menu.xsd">

    <menu id="sales" insertBefore="administration">
        <item id="sales_Order.lookup"/>

        <separator/>

        <item id="sales_Customer.lookup" openType="DIALOG"/> (1)

        <item screen="sales_CustomerInfo">
            <properties>
                <property name="stringParam" value="some string"/> (2)
                <property name="customerParam" (3)
                          entityClass="com.company.demo.entity.Customer"
                          entityId="0118cfbe-b520-797e-98d6-7d54146fd586"/>
            </properties>
        </item>

        <item screen="sales_Customer.edit">
            <properties>
                <property name="entityToEdit" (4)
                          entityClass="com.company.demo.entity.Customer"
                          entityId="0118cfbe-b520-797e-98d6-7d54146fd586"
                          entityView="_local"/>
            </properties>
        </item>
    </menu>

</menu-config>
1 - open the screen in dialog window.
2 - invoke setStringParam() method passing some string to it.
3 - invoke setCustomerParam() method passing an entity instance loaded by the given id.
4 - invoke setEntityToEdit() method of StandardEditor passing an entity instance loaded by the given id and view.
menu-config.sales=Sales
menu-config.sales_Customer.lookup=Customers

If the id is not set, the name of the menu element will be generated from the class name (if the class attribute is set) or the bean name and the bean method name (if the bean attribute is set), therefore setting the id attribute is recommended.

A.6. metadata.xml

Files of this type are used for registering custom datatypes and non-persistent entities and assigning meta annotations.

The metadata.xml file of the project is specified in the cuba.metadataConfig application property.

The file has the following structure:

metadata – root element.

metadata elements:

  • datatypes - an optional descriptor of custom datatypes.

    datatypes elements:

    • datatype - the datatype descriptor. It has the following attributes:

      • id - identifier, which should be used to refer to this datatype from @MetaProperty annotation.

      • class - defines the implementation class.

      • sqlType - optional attribute which specifies an SQL type of your database suitable for storing values of this data type. The SQL type will be used by CUBA Studio when it generates database scripts. See Example of a Custom Datatype for details.

      The datatype element can also contain other attributes that depend on the implementation of the datatype.

  • metadata-model – the project’s meta model descriptor.

    metadata-model attribute:

    • root-package – the project’s root package.

      metadata-model elements:

    • class – a non-persistent entity class.

  • annotations – contains assignments of entity meta-annotations.

    The annotations element contains entity elements which define entities to assign meta-annotation to. Each entity element must contain the class attribute which specifies an entity class, and a list of annotation elements.

    The annotation element defines a meta-annotation. It has the name attribute which corresponds to the meta-annotation name. The map of meta-annotation attributes is defined using the list of nested attribute elements.

Example:

<metadata xmlns="http://schemas.haulmont.com/cuba/metadata.xsd">

    <metadata-model root-package="com.sample.sales">
        <class>com.sample.sales.entity.SomeNonPersistentEntity</class>
        <class>com.sample.sales.entity.OtherNonPersistentEntity</class>
    </metadata-model>

    <annotations>
        <entity class="com.haulmont.cuba.security.entity.User">
            <annotation name="com.haulmont.cuba.core.entity.annotation.TrackEditScreenHistory">
                <attribute name="value" value="true" datatype="boolean"/>
            </annotation>

            <annotation name="com.haulmont.cuba.core.entity.annotation.EnableRestore">
                <attribute name="value" value="true" datatype="boolean"/>
            </annotation>
        </entity>

        <entity class="com.haulmont.cuba.core.entity.Category">
            <annotation name="com.haulmont.cuba.core.entity.annotation.SystemLevel">
                <attribute name="value" value="false" datatype="boolean"/>
            </annotation>
        </entity>
    </annotations>

</metadata>

A.7. permissions.xml

Files of this type are used in the Web Client block for registration of specific user permissions.

The file location is defined in the cuba.permissionConfig application property. When you create a new project in Studio, it creates the web-permissions.xml file in the root package of the web module, for example modules/web/src/com/company/sample/web-permissions.xml.

The file has the following structure:

permission-config - root element.

permission-config elements:

  • specific - specific permissions descriptor.

    specific elements:

    • category - permissions category which is used for grouping permissions in the role edit screen. id attribute is used as a key for retrieving a localized category name from the main message pack.

    • permission - named permission. id attribute is used to obtain the permission value by the Security.isSpecificPermitted() method, and as a key for retrieving a localized permission name form the main message pack to display the permission in the role edit screen.

For example:

<permission-config xmlns="http://schemas.haulmont.com/cuba/permissions.xsd">
    <specific>
        <category id="app">
            <permission id="app.doSomething"/>
            <permission id="app.doSomethingOther"/>
        </category>
    </specific>
</permission-config>

A.8. persistence.xml

Files of this type are standard for JPA, and are used for registration of persistent entities and configuration of ORM framework parameters.

The persistence.xml file of the project is defined in the cuba.persistenceConfig application property.

When the Middleware block starts, the specified files are combined into a single persistence.xml, stored in the application work folder. File order is important, because each subsequent file in the list can override previously defined ORM parameters.

Example of a file:

<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="1.0">
  <persistence-unit name="sales" transaction-type="RESOURCE_LOCAL">
      <class>com.sample.sales.entity.Customer</class>
      <class>com.sample.sales.entity.Order</class>
  </persistence-unit>
</persistence>

A.9. remoting-spring.xml

Files of this type configure an additional Spring Framework container for the Middleware block, used for exporting services and other middleware components accessed by the client tier (hereafter remote access container).

The remoting-spring.xml file of the project is specified in the cuba.remotingSpringContextConfig application property.

Remote access container is created with the main container (configured by spring.xml files) as its parent. Therefore, the beans of the remote access container can use the beans of the main container, while the beans of the main container cannot "see" the beans of the remote access container.

The primary goal of remote access is to make Middleware services accessible to the client level using the Spring HttpInvoker mechanism. The cuba-remoting-spring.xml file in the cuba application component defines the servicesExporter bean of RemoteServicesBeanCreator type, which receives all service classes from the main container and exports them. In addition to regular annotated services, remote access container exports a number of specific beans, such as AuthenticationService.

Furthermore, the cuba-remoting-spring.xml file defines a base package that serves as a starting point for lookup of annotated Spring MVC controller classes used for file uploading and downloading.

The remoting-spring.xml file in the application project should only be created when specific Spring MVC controllers are used. Application project services will be imported by the standard servicesExporter bean defined in the cuba application component.

A.10. spring.xml

The files of this type configure the main Spring Framework container for each application block.

The spring.xml file of the project is specified in the cuba.springContextConfig application property.

Most of the configuration of the main container is performed using bean annotations (e.g. @Component, @Service, @Inject and others), therefore the only mandatory part of spring.xml in an application project is the context:component-scan element, which specifies the base Java package for lookup of annotated classes. For example:

<context:component-scan base-package="com.sample.sales"/>

The remaining configuration depends on the block that a container is being configured for, e.g. the registration of JMX-beans for the Middleware block, or services import for client blocks.

A.11. views.xml

Files of this type are used to describe views, see Views.

XML schema is available at http://schemas.haulmont.com/cuba/7.1/view.xsd.

views – root element.

views elements:

  • viewview descriptor.

    view attributes:

    • class – entity class.

    • entity – the name of the entity, for example sales_Order. This attribute can be used instead of the class attribute.

    • name – view name, unique within the entity.

    • systemProperties – enables inclusion of system attributes defined in base interfaces for persistent entities BaseEntity and Updatable. Optional attribute, true by default.

    • overwrite – enables overriding a view with the same class and name already deployed in the repository. Optional attribute, false by default.

    • extends – specifies an entity view, from which the attributes should be inherited. For example, declaring extends="_local", will add all local attributes of an entity to the current view. Optional attribute.

    view elements:

    • propertyViewProperty descriptor.

    property attributes:

    • name – entity attribute name.

    • view – for reference type attributes, specifies a view name the associated entity should be loaded with.

    • fetch - for reference attributes, specifies how to fetch the related entity from the database. See Views for details.

    property elements:

    • property – associated entity attribute descriptor. This enables defining an unnamed inline view for an associated entity in the current descriptor.

  • include – include another views.xml file.

    include attributes:

    • file – file path according to the Resources interface rules.

Example:

<views xmlns="http://schemas.haulmont.com/cuba/view.xsd">

  <view class="com.sample.sales.entity.Order"
        name="order-with-customer"
        extends="_local">
      <property name="customer" view="_minimal"/>
  </view>

  <view class="com.sample.sales.entity.Item"
        name="itemsInOrder">
      <property name="quantity"/>
      <property name="product" view="_minimal"/>
  </view>

  <view class="com.sample.sales.entity.Order"
        name="order-with-customer-defined-inline"
        extends="_local">
      <property name="customer">
          <property name="name"/>
          <property name="email"/>
      </property>
  </view>

</views>

See also the cuba.viewsConfig application property.

A.12. web.xml

The web.xml file is a standard descriptor of a Java EE web application and should be created for the Middleware, Web Client and Web Portal blocks.

In an application project, web.xml files are located in the web/WEB-INF folders of the corresponding modules.

  • web.xml for the Middleware block (core project module) has the following content:

    <?xml version="1.0" encoding="UTF-8" standalone="no"?>
    <web-app xmlns="http://java.sun.com/xml/ns/javaee"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
             version="3.0">
        <!-- Application properties config files -->
        <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>
        <!--Application components-->
        <context-param>
            <param-name>appComponents</param-name>
            <param-value>com.haulmont.cuba com.haulmont.reports</param-value>
        </context-param>
        <listener>
            <listener-class>com.haulmont.cuba.core.sys.AppContextLoader</listener-class>
        </listener>
        <servlet>
            <servlet-name>remoting</servlet-name>
            <servlet-class>com.haulmont.cuba.core.sys.remoting.RemotingServlet</servlet-class>
            <load-on-startup>1</load-on-startup>
        </servlet>
        <servlet-mapping>
            <servlet-name>remoting</servlet-name>
            <url-pattern>/remoting/*</url-pattern>
        </servlet-mapping>
    </web-app>

    The context-param elements define initializing parameters for the ServletContext object of the current web application. The list of application components is defined in the appComponents parameter, the list of application property files is defined in the appPropertiesConfig parameter.

    The listener element defines a listener class implementing the ServletContextListener interface. The Middleware block uses the AppContextLoader class as a listener. This class initializes the AppContext.

    Servlet descriptions follow, including the RemotingServlet class, mandatory for the Middleware block. This servlet is accessible via the /remoting/* URL, and is related to the remote access container (see remoting-spring.xml).

  • web.xml for the Web Client block (web project module) has the following content:

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns="http://java.sun.com/xml/ns/javaee"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
             version="3.0">
    
        <!-- Application properties config files -->
        <context-param>
            <param-name>appPropertiesConfig</param-name>
            <param-value>
                classpath:com/company/demo/web-app.properties
                /WEB-INF/local.app.properties
                "file:${catalina.base}/conf/app/local.app.properties"
            </param-value>
        </context-param>
        <!--Application components-->
        <context-param>
            <param-name>appComponents</param-name>
            <param-value>com.haulmont.cuba com.haulmont.reports</param-value>
        </context-param>
    
        <listener>
            <listener-class>com.vaadin.server.communication.JSR356WebsocketInitializer</listener-class>
        </listener>
        <listener>
            <listener-class>com.haulmont.cuba.web.sys.WebAppContextLoader</listener-class>
        </listener>
    
        <servlet>
            <servlet-name>app_servlet</servlet-name>
            <servlet-class>com.haulmont.cuba.web.sys.CubaApplicationServlet</servlet-class>
            <async-supported>true</async-supported>
        </servlet>
        <servlet>
            <servlet-name>dispatcher</servlet-name>
            <servlet-class>com.haulmont.cuba.web.sys.CubaDispatcherServlet</servlet-class>
            <load-on-startup>1</load-on-startup>
        </servlet>
        <servlet-mapping>
            <servlet-name>dispatcher</servlet-name>
            <url-pattern>/dispatch/*</url-pattern>
        </servlet-mapping>
        <servlet-mapping>
            <servlet-name>app_servlet</servlet-name>
            <url-pattern>/*</url-pattern>
        </servlet-mapping>
    
        <filter>
            <filter-name>cuba_filter</filter-name>
            <filter-class>com.haulmont.cuba.web.sys.CubaHttpFilter</filter-class>
            <async-supported>true</async-supported>
        </filter>
        <filter-mapping>
            <filter-name>cuba_filter</filter-name>
            <url-pattern>/*</url-pattern>
        </filter-mapping>
    </web-app>

    In the context-param elements, the lists of application components and application property files are defined.

    The Web Client block uses the WebAppContextLoader class as a ServletContextListener.

    The JSR356WebsocketInitializer listener is required for WebSockets protocol support.

    CubaApplicationServlet provides the generic user interface implementation based on the Vaadin framework.

    CubaDispatcherServlet initializes an additional Spring context for Spring MVC controllers. This context is configured in the dispatcher-spring.xml file.

Appendix B: Application Properties

This appendix describes all available application properties in alphabetical order.

cuba.additionalStores

Defines the names of additional data stores used in the application.

Used in all standard blocks.

Example:

cuba.additionalStores = db1, mem1
cuba.allowQueryFromSelected

Allows the generic filter to use sequential filtering mode. See also Sequential Queries.

Default value: true

Stored in the database.

Interface: GlobalConfig

Used in the Web Client and the Middleware blocks.

cuba.anonymousLogin

Login name of the user on behalf of which the anonymous session is created (see cuba.anonymousSessionId).

Default value: anonymous

Stored in the database.

Interface: ServerConfig

Used in the Middleware block.

cuba.anonymousSessionId

Defines the UUID of the anonymous user session which is available before user login. This session is always created automatically on the server startup. See also cuba.anonymousLogin.

Interface: GlobalConfig

Used in all standard blocks.

cuba.automaticDatabaseUpdate

Determines whether server should run DB update scripts at application start.

Default value: false

Stored in the database.

Interface: ServerConfig

Used in the Middleware block.

cuba.availableLocales

List of supported user interface languages.

Property format: {language_name1}|{language_code_1};{language_name2}|{language_code_2};…​

Example:

cuba.availableLocales=French|fr;English|en

{language_name} – name displayed in the list of available languages. For example, such lists can be found on the login screen and on the user edit screen.

{language_code} – corresponds to language code returned by the Locale.getLanguage() method. Used as a suffix for message pack file names. For example, messages_fr.properties.

The first language listed in the cuba.availableLocales property will be selected in the list of available languages by default if the list does not contain the user’s operating system language. Otherwise, user’s operating system language will be selected by default.

Default value: English|en;Russian|ru;French|fr

Interface: GlobalConfig

Used in all standard blocks.

cuba.backgroundWorker.maxActiveTasksCount

The maximum number of active background tasks.

Default value: 100

Interface: WebConfig

Used in the Web Client block.

cuba.backgroundWorker.timeoutCheckInterval

Defines an interval in milliseconds for checking timeouts of background tasks.

Default value: 5000

Interface: ClientConfig

Used in Web Client.

cuba.bruteForceProtection.enabled

Enables a mechanism for the protection against password brute force cracking.

Default value: false

Stored in the database.

Interface: ServerConfig

Used in the Middleware block.

cuba.bruteForceProtection.blockIntervalSec

Blocking interval in seconds after exceeding a maximum number of failed login attempts, if the cuba.bruteForceProtection.enabled property is on.

Default value: 60

Stored in the database.

Interface: ServerConfig

Used in the Middleware block.

cuba.bruteForceProtection.maxLoginAttemptsNumber

A maximum number of failed login attempts for the combination of user login and IP address, if the cuba.bruteForceProtection.enabled property is on.

Default value: 5

Stored in the database.

Interface: ServerConfig

Used in the Middleware block.

cuba.checkConnectionToAdditionalDataStoresOnStartup

If set to true, the framework checks connections to all additional data stores on the application startup. If the connection fails, a message is written in the log. Keep in mind that such checks can slow down the startup process.

Default value: false

Used in the Middleware block.

cuba.checkPasswordOnClient

When set to false (which is the default), LoginPasswordLoginProvider of the client block sends user’s password as is to the middleware AuthenticationService.login() method. This is appropriate if the client and middleware blocks are co-located in the same JVM. For a distributed deployment when the client block is located on a different computer on the network, the connection between the client and middleware in this case should be encrypted using SSL.

If set to true, LoginPasswordLoginProvider loads the User entity by the entered login and checks the password itself. If the password matches the loaded password hash, the provider performs login as a trusted client using the password specified in the cuba.trustedClientPassword property. This mode saves you from setting up SSL connections between clients and middleware in trusted networks, and at the same time does not exposes user passwords to the network: only hashes are transmitted. However notice that trusted client password is still transmitted over the network, so the SSL-protected connection is still more secure.

Default value: false

Interface: WebAuthConfig, PortalConfig

Used in the Web and Portal blocks.

cuba.cluster.enabled

Enables interaction between Middleware servers in a cluster. See Configuring Interaction between Middleware Servers for details.

Default value: false

Used in the Middleware block.

cuba.cluster.jgroupsConfig

Path to JGroups configuration file. The file is loaded using the Resources interface, so it can be located in classpath or in the configuration directory.

For example:

cuba.cluster.jgroupsConfig = my_jgroups_tcp.xml

Default value: jgroups.xml

Used in the Middleware block.

cuba.cluster.messageSendingQueueCapacity

Limits the queue of middleware cluster messages. When the queue exceeds its maximum size, new messages are rejected.

Default value: Integer.MAX_VALUE

Used in the Middleware block.

cuba.cluster.stateTransferTimeout

Sets the timeout in milliseconds for receiving state from cluster on node start.

Default value: 10000

Used in the Middleware block.

cuba.confDir

Defines location of the configuration folder for an application block.

Default value for Fast Deployment in Tomcat: ${catalina.home}/conf/${cuba.webContextName}, which points to a subdirectory with the name of the web app inside the tomcat/conf folder, for example, tomcat/conf/app-core.

Default value for WAR and UberJAR deployment: ${app.home}/${cuba.webContextName}/conf, which points to a subdirectory of the application home.

Interface: GlobalConfig

Used in all standard blocks.

cuba.connectionReadTimeout

Sets Middleware connection timeout for client blocks. Non-negative value is passed to the setReadTimeout() method of URLConnection.

Default value: -1

Used in the Web Client and Web Portal blocks.

cuba.connectionTimeout

Sets Middleware connection timeout for client blocks. Non-negative value is passed to the setConnectTimeout() method of URLConnection.

Default value: -1

Used in the Web Client and Web Portal blocks.

cuba.connectionUrlList

Sets Middleware server connection URL for client blocks.

Property value should contain one or more comma separated URLs http[s]://host[:port]/app-core, where host is the server hostname, port is the server port, and app-core is the name of the the Middleware block web application. For example:

cuba.connectionUrlList = http://localhost:8080/app-core

When using a cluster of Middleware servers, their addresses should be listed separated with commas:

cuba.connectionUrlList = http://server1:8080/app-core,http://server2:8080/app-core

Interface: ClientConfig

Used in the Web Client and Web Portal blocks.

cuba.creditsConfig

Additive property defining a credits.xml file containing information about the software components used by the application.

The file is loaded using the Resources interface, so it can be located in classpath or in the configuration directory.

Used in the Web Client block.

Example:

cuba.creditsConfig = +com/company/base/credits.xml
cuba.crossDataStoreReferenceLoadingBatchSize

Batch size for loading related entities from different data stores by DataManager.

Default value: 50

Stored in the database.

Interface: ServerConfig

Used in the Middleware block.

cuba.dataManagerBeanValidation

Indicates that DataManager should perform bean validation when saving entities.

Default value: false

Stored in the database.

Interface: ServerConfig

Used in the Middleware block.

cuba.dataManagerChecksSecurityOnMiddleware

Indicates that DataManager should check entity operation permissions and in-memory constraints on the middle tier.

Default value: false

Stored in the database.

Interface: ServerConfig

Used in the Middleware block.

cuba.dataSourceJndiName

Defines JNDI name of the javax.sql.DataSource object used for connection to the application database.

Default value: java:comp/env/jdbc/CubaDS

Used in the Middleware block.

cuba.dataDir

Defines the location of the work folder for an application block.

Default value for Fast Deployment in Tomcat: ${catalina.home}/work/${cuba.webContextName}, which points to a subdirectory with the name of the web app inside the tomcat/work folder, for example, tomcat/work/app-core.

Default value for WAR and UberJAR deployment: ${app.home}/${cuba.webContextName}/work, which points to a subdirectory of the application home.

Interface: GlobalConfig

Used in all standard blocks.

cuba.dbDir

Defines the location of the database scripts directory.

Default value for Fast Deployment in Tomcat: ${catalina.home}/webapps/${cuba.webContextName}/WEB-INF/db, which points to the WEB-INF/db subdirectory of the web application in Tomcat.

Default value for WAR and UberJAR deployment: web-inf:db, which points to the WEB-INF/db subdirectory inside the WAR or UberJAR.

Interface: ServerConfig

Used in the Middleware block.

cuba.dbmsType

Defines the DBMS type. Affects the choice of DBMS integration interface implementations and the search for database init and update scripts together with cuba.dbmsVersion.

See DBMS Types for details.

Default value: hsql

Used in the Middleware block.

cuba.dbmsVersion

An optional property that sets the database version. Affects the choice of DBMS integration interface implementations and the search for database init and update scripts together with cuba.dbmsType.

See DBMS Types for details.

Default value: none

Used in the Middleware block.

cuba.defaultPermissionValuesConfig

Defines the set of files with the user’s default permissions. Default permission values are used when no role defines an explicit value for permission target. Used mostly for denying roles, see more in the default-permission-values.xml section.

Default value: cuba-default-permission-values.xml

Used in the Middleware block.

Example:

cuba.defaultPermissionValuesConfig = +my-default-permission-values.xml
cuba.defaultQueryTimeoutSec

Defines default transaction timeout.

Default value: 0 (no timeout).

Stored in the database.

Interface: ServerConfig

Used in the Middleware block.

cuba.disableEntityEnhancementCheck

Disables the startup check which ensures all entities are properly enhanced.

Default value: true

Interface: ServerConfig

Used in the Middleware block.

cuba.disableEscapingLikeForDataStores

Contains a list of data stores for which the platform should disable ESCAPE in JPQL queries with LIKE operator in filters.

Stored in the database.

Interface: GlobalConfig

Used in all standard blocks.

cuba.disableOrmXmlGeneration

Disables automatic generation of the orm.xml file for extended entities.

Default value: false (orm.xml will be created automatically if any extended entity exists).

Used in the Middleware block.

cuba.dispatcherSpringContextConfig

Additive property defining a dispatcher-spring.xml file of a client block.

The file is loaded using the Resources interface, so it can be located in classpath or in the configuration directory.

Used in the Web Client and Web Portal blocks.

Example:

cuba.dispatcherSpringContextConfig = +com/company/sample/portal-dispatcher-spring.xml
cuba.download.directories

Defines a list of folders from which the Middleware files can be downloaded from via com.haulmont.cuba.core.controllers.FileDownloadController. For example, file downloading is utilized by the server log display mechanism found in the Administration > Server Log web client screen.

The folder list should be separated with a semicolon.

Default value: ${cuba.tempDir};${cuba.logDir} (files can be downloaded from the temporary folder and the logs folder).

Used in the Middleware block.

cuba.email.*

Email sending parameters described in Configuring Email Sending Parameters.

cuba.fileStorageDir

Defines file storage folder structure roots. For more information, see Standard File Storage Implementation.

Default value: null

Interface: ServerConfig

Used in the Middleware block.

cuba.enableDeleteStatementInSoftDeleteMode

Backward compatibility toggle. If set to true, enables running JPQL delete from statement for soft-deleted entities when soft deletion mode is on. Such statement is transformed to SQL which deletes all instances not marked for deletion. This is counter-intuitive and disabled by default.

Default value: false

Used in the Middleware block.

cuba.enableSessionParamsInQueryFilter

Backward compatibility toggle. If set to false, the filter conditions in datasource query filters and Filter component will be applied once at least one parameter value is supplied; the session parameters will not work.

Default value: true

Used in the Web Client block.

cuba.entityAttributePermissionChecking

If set to true, DataManager checks entity attribute permissions. When false, attribute permissions are checked only in Generic UI data-aware components and REST API endpoints.

Default value: false

Stored in the database.

Used in the Middleware block.

cuba.entityLog.enabled

Activates the entity log mechanism.

Default value: true

Stored in the database.

Interface: EntityLogConfig

Used in the Middleware block.

cuba.groovyEvaluationPoolMaxIdle

Sets the maximum number of unused compiled Groovy expressions in the pool during Scripting.evaluateGroovy() method execution. It is recommended to increment this parameter when intensive execution of Groovy expressions is required, for example, for a large number of application folders.

Default value: 8

Used in all standard blocks.

cuba.groovyEvaluatorImport

Defines a list of classes imported by all Groovy expressions executed through Scripting.

Class names in the list should be separated with commas or semicolons.

Default value: com.haulmont.cuba.core.global.PersistenceHelper

Used in all standard blocks.

Example:

cuba.groovyEvaluatorImport = com.haulmont.cuba.core.global.PersistenceHelper,com.abc.sales.CommonUtils
cuba.gui.genericFilterApplyImmediately

When set to true, the generic filter works in the immediate mode when every change of filter parameters automatically reloads data. When set to false, the filter will be applied only after the Search button is clicked. See also applyImmediately filter attribute.

Default value: true

Stored in the database.

Interface: ClientConfig

Used in the Web Client block.

cuba.gui.genericFilterChecking

Influences the behavior of the Filter component.

When set to true, does not allow to apply a filter without specifying parameters.

Default value: false

Stored in the database.

Interface: ClientConfig

Used in the Web Client block.

cuba.gui.genericFilterColumnsCount

Defines the number of columns with conditions for the Filter component.

Default value: 3

Stored in the database.

Interface: ClientConfig

Used in the Web Client block.

cuba.gui.genericFilterConditionsLocation

Defines the location of the conditions panel in the Filter component. Two locations are available: top (above the filter control elements) and bottom (below the filter control elements).

Default value: top

Stored in the database.

Interface: ClientConfig

Used in the Web Client block.

cuba.gui.genericFilterControlsLayout

Sets a template for Filter controls layout. Each control has the following format: [component_name | options-comma-separated], e.g. [pin | no-caption, no-icon].

Available controls:

  • filters_popup - popup button for selecting a filter, combined with the Search button.

  • filters_lookup - lookup field for selecting a filter. The Search button should be added as a separate control.

  • search - Search button. Do not add if use filters_popup.

  • add_condition - link button for adding new conditions.

  • spacer - an empty space between controls.

  • settings - Settings button. Specify action names that should be displayed in Settings popup as options (see below).

  • max_results - group of controls for setting the maximum number of records to be selected.

  • fts_switch - checkbox for switching to the Full-Text Search mode.

The following actions can be used as options of the settings control: save, save_as, edit, remove, pin, make_default, save_search_folder, save_app_folder, clear_values.

The actions can also be used as independent controls outside of the Settings popup. In this case, they can have the following options:

  • no-icon - if an action button should be displayed without an icon. For example: [save | no-icon].

  • no-caption - if an action button should be displayed without a caption. For example: [pin | no-caption].

Default value:

[filters_popup] [add_condition] [spacer] \
[settings | save, save_as, edit, remove, make_default, pin, save_search_folder, save_app_folder, clear_values] \
[max_results] [fts_switch]

Stored in the database.

Interface: ClientConfig

Used in the Web Client block.

cuba.gui.genericFilterManualApplyRequired

Influences the behavior of the Filter component.

When set to true, screens containing filters will not trigger corresponding data loaders automatically on opening, until the user clicks the filter’s Apply button.

The value of cuba.gui.genericFilterManualApplyRequired is ignored, when opening browser screens using an application or search folders, i.e. the filter is applied. The filter will not be applied, if the applyDefault value for a folder is explicitly set to false.

Default value: false

Stored in the database.

Interface: ClientConfig

Used in the Web Client block.

cuba.gui.genericFilterMaxResultsOptions

Defines the options for the Show rows drop-down list of the Filter component.

NULL option indicates that the list should contain an empty value.

Default value: NULL, 20, 50, 100, 500, 1000, 5000

Stored in the database.

Interface: ClientConfig

Used in the Web Client block.

cuba.gui.genericFilterPopupListSize

Defines the number of items displayed in the popup list of the Search button. If the number of filters exceeds this value, Show more…​ action is added to the popup list. The action opens a new dialog window with a list of all possible filters.

Default value: 10

Stored in the database.

Interface: ClientConfig

Used in the Web Client block.

cuba.gui.genericFilterTrimParamValues

Defines whether all generic filters should trim input values. When set to false, the text filter will not trim values.

Default value: true

Stored in the database.

Interface: ClientConfig

Used in the Web Client block.

cuba.gui.layoutAnalyzerEnabled

Allows you to disable the screen analyzer available in the context menu of the main window tabs and the modal window captions.

Default value: true

Stored in the database.

Interface: ClientConfig

Used in the Web Client block.

cuba.gui.lookupFieldPageLength

Defines the default number of options on one page of the drop-down list in the LookupField and LookupPickerField components. It can be overridden for a concrete instance of the component using the pageLength XML attribute.

Default value: 10

Stored in the database.

Interface: ClientConfig

Used in the Web Client.

cuba.gui.manualScreenSettingsSaving

If the property is set to true, screens will not save their settings automatically on close. In this mode, a user can save or reset settings using the context menu which appears on clicking a screen tab or a dialog window caption.

Default value: false

Interface: ClientConfig

Stored in the database.

Used in the Web Client block.

cuba.gui.showIconsForPopupMenuActions

Enables displaying action icons in Table context menu and PopupButton items.

Default value: false

Stored in the database.

Interface: ClientConfig

Used in the Web Client block.

cuba.gui.systemInfoScriptsEnabled

Enables the display of SQL-scripts for creating / updating / retrieving an entity instance in the System Information window.

Such scripts actually show the contents of the database rows that store the selected entity instance, regardless of security settings that may deny viewing of some attributes. That is why it is reasonable to revoke the CUBA / Generic UI / System Information specific permission for all user roles except the administrators, or set the cuba.gui.systemInfoScriptsEnabled to false for the whole application.

Default value: true

Stored in the database.

Interface: ClientConfig

Used in the Web Client block.

cuba.gui.useSaveConfirmation

Defines the layout of the dialog displayed when a user attempts closing a screen with unsaved changes in datasources.

Value of true corresponds to a layout with three possible actions: Save changes, Don’t Save, Don’t close the screen.

The value of false corresponds to a form with two options: Close the screen without saving changes, Don’t close the screen.

Default value: true

Stored in the database.

Interface: ClientConfig

Used in the Web Client block.

cuba.gui.validationNotificationType

Defines the standard window validation error validation error notification type.

Possible values are the elements of com.haulmont.cuba.gui.components.Frame.NotificationType enumeration:

  • TRAY - tray popup with plain text message,

  • TRAY_HTML - tray popup with HTML message,

  • HUMANIZED - standard popup with plain text message,

  • HUMANIZED_HTML - standard popup with HTML message,

  • WARNING - warning popup with plain text message,

  • WARNING_HTML - warning popup with HTML message,

  • ERROR - error popup with plain text message,

  • ERROR_HTML - error popup with HTML message.

Default value: TRAY.

Interface: ClientConfig

Used in the Web Client block.

cuba.hasMultipleTableConstraintDependency

Enables using JOINED inheritance strategy for composite entities. If set to true, provides the correct order of inserting new entities in the database.

Default value: false

cuba.healthCheckResponse

Defines the text returned from a request to the health check URL.

Default value: ok

Interface: GlobalConfig

Used in all standard blocks.

cuba.httpSessionExpirationTimeoutSec

Defines HTTP-session inactivity timeout in seconds.

Default value: 1800

Interface: WebConfig

Used in the Web Client block.

It is recommended to use the same values for cuba.userSessionExpirationTimeoutSec and cuba.httpSessionExpirationTimeoutSec properties.

Do not try to set HTTP session timeout in web.xml - it will be ignored.

cuba.iconsConfig

Additive property defining icon sets.

Used in the Web Client block.

Example:

cuba.iconsConfig = +com.company.demo.web.MyIconSet
cuba.inMemoryDistinct

Enables in-memory filtering of duplicate records instead of using select distinct at the database level. Used by the DataManager.

Default value: false

Stored in the database.

Interface: ServerConfig

Used in the Middleware block.

cuba.jmxUserLogin

Defines a user login that should be used for system authentication.

Default value: admin

Used in the Middleware block.

cuba.keyForSecurityTokenEncryption

Used as a key for AES encryption of the entity security token. The token is sent inside an entity instance when it is loaded from the middleware in the following cases:

Although the security token does not contain any attribute values (only attribute names and filtered entity identifiers), it is highly recommended to change the default value of the encryption key in the production environment.

Default value: CUBA.Platform

Interface: ServerConfig

Used in the Middleware block.

cuba.numberIdCacheSize

When an instance of entity inherited from BaseLongIdEntity or BaseIntegerIdEntity is created in memory via Metadata.create() method, an identifier value is assigned to the entity right away. This value is obtained from the mechanism that fetches the next id from a database sequence. In order to reduce the number of middleware and database calls, the sequence’s increment is set by default to 100, which means that the framework obtains the range of ids on each invocation. So it "caches" this range and yields the ids without going for the next value to the database until the whole range is used.

The property defines the sequence’s increment and the corresponding size of the cached range in memory.

If you change the value of this property when there are already some entities in the database, recreate also all existing sequences with the new increment (which must be equal to cuba.numberIdCacheSize) and the starting values corresponding to the maximum values of existing ids.

Do not forget to set the property on all blocks used in the application. For example, if you have Web Client, Portal Client and Middleware, you should set the same value in web-app.properties, portal-app.properties and app.properties.

Default value: 100

Interface: GlobalConfig

Used in all standard blocks.

cuba.legacyPasswordEncryptionModule

Same as cuba.passwordEncryptionModule but defines the name of the bean used for user password hashing for users created before migration to the framework version 7 and having the SEC_USER.PASSWORD_ENCRYPTION field empty.

Default value: cuba_Sha1EncryptionModule

Used in all standard blocks.

cuba.localeSelectVisible

Disables the user interface language selection when logging in.

If cuba.localeSelectVisible is set to false, the locale for a user session is selected in the following way:

  • If the User entity instance has a language attribute defined, the system will use this language.

  • If the user’s operating system language is included in the list of available locales (set by the cuba.availableLocales property), the system will use this language.

  • Otherwise, the system will use the first language defined in the cuba.availableLocales property.

Default value: true

Interface: GlobalConfig

Used in all standard blocks.

cuba.logDir

Defines the location of the log folder for an application block.

Default value for Fast Deployment in Tomcat: ${catalina.home}/logs, which points to the tomcat/logs folder.

Default value for WAR and UberJAR deployment: ${app.home}/logs, which points to the logs subdirectory of the application home.

Interface: GlobalConfig

Used in all standard blocks.

cuba.mainMessagePack

Additive property defining a main message pack for an application block.

The value may include a single pack or a list of packs separated with spaces.

Used in all standard blocks.

Example:

cuba.mainMessagePack = +com.company.sample.gui com.company.sample.web
cuba.maxUploadSizeMb

Maximum file size (in megabytes) that can be uploaded using the FileUploadField and FileMultiUploadField components.

Default value: 20

Stored in the database.

Interface: ClientConfig

Used in the Web Client block.

cuba.menuConfig

Additive property defining a menu.xml file.

The file is loaded using the Resources interface, so it can be located in classpath or in the configuration directory.

Used in the Web Client block.

Example:

cuba.menuConfig = +com/company/sample/web-menu.xml
cuba.metadataConfig

Additive property defining a metadata.xml file.

The file is loaded using the Resources interface, so it can be located in classpath or in the configuration directory.

Used in all standard blocks.

Example:

cuba.metadataConfig = +com/company/sample/metadata.xml
cuba.passwordEncryptionModule

Defines the name of the bean used for user password hashing. When creating new user or updating user’s password, the value of this property is stored for the user in the SEC_USER.PASSWORD_ENCRYPTION database field.

Default value: cuba_BCryptEncryptionModule

Used in all standard blocks.

cuba.passwordPolicyEnabled

Enables password policy enforcement. If the property is set to true, all new user passwords will be checked according to the cuba.passwordPolicyRegExp property.

Default value: false

Stored in the database.

Interface: ClientConfig

Used in the client blocks: Web Client, Web Portal.

cuba.passwordPolicyRegExp

Defines a regular expression used by the password checking policy.

Default value:

((?=.*\\d)(?=.*\\p{javaLowerCase}) (?=.*\\p{javaUpperCase}).{6,20})

The expression above ensures that password contains from 6 to 20 characters, uses numbers and Latin letters, contains at least one number, one lower case, and one upper case letter. More information on regular expression syntax is available at https://en.wikipedia.org/wiki/Regular_expression and http://docs.oracle.com/javase/6/docs/api/java/util/regex/Pattern.html.

Stored in the database.

Interface: ClientConfig

Used in the client level blocks: Web Client, Web Portal.

cuba.performanceLogDisabled

Must be set to true in case you need to disable PerformanceLogInterceptor.

PerformanceLogInterceptor is triggered by the @PerformanceLog annotation of a class or a method, which provides logging of each method invocation and its execution time in the perfstat.log file. If you don’t need such logging, we recommend you to disable PerformanceLogInterceptor for performance reasons. To enable it again, delete this property or set the value to false.

Default value: false

Used in the Middleware block.

cuba.performanceTestMode

Must be set to true when the application is running performance tests.

Interface: GlobalConfig

Default value: false

Used in Web Client and Middleware blocks.

cuba.permissionConfig

Additive property defining a permissions.xml file.

Used in the Web Client block.

Example:

cuba.permissionConfig = +com/company/sample/web-permissions.xml
cuba.persistenceConfig

Additive property defining a persistence.xml file.

The file is loaded using the Resources interface, so it can be located in classpath or in the configuration directory.

Used in all standard blocks.

Example:

cuba.persistenceConfig = +com/company/sample/persistence.xml
cuba.portal.anonymousUserLogin

Defines a user login that should be used for anonymous session in the Web Portal block.

The user with the specified login should exist in the security subsystem and should have the required permissions. User password is not required, because anonymous portal sessions are created via the loginTrusted() method with the password defined in the cuba.trustedClientPassword property.

Interface: PortalConfig

Used in the Web Portal block.

cuba.queryCache.enabled

If set to false, the query cache functionality is disabled.

Default value: true

Interface: QueryCacheConfig

Used in the Middleware block.

cuba.queryCache.maxSize

Maximum number of query cache entries. A cache entry is defined by the query text, query parameters, paging parameters and soft deletion.

As the cache size grows close to the maximum, the cache evicts entries that are less likely to be used again.

Default value: 100

Interface: QueryCacheConfig

Used in the Middleware block.

cuba.remotingSpringContextConfig

Additive property defining a remoting-spring.xml file of the Middleware block.

The file is loaded using the Resources interface, so it can be located in classpath or in the configuration directory.

Used in the Middleware block.

Example:

cuba.remotingSpringContextConfig = +com/company/sample/remoting-spring.xml
cuba.schedulingActive

Enables the CUBA scheduled tasks mechanism.

Default value: false

Stored in the database.

Interface: ServerConfig

Used in the Middleware block.

cuba.serialization.impl

Specifies an implementation of the Serialization interface which is used for serialization of objects transferred between the application blocks. The platform contains two implementations:

  • com.haulmont.cuba.core.sys.serialization.StandardSerialization - standard Java serialization.

  • com.haulmont.cuba.core.sys.serialization.KryoSerialization - serialization based on the Kryo framework.

Default value: com.haulmont.cuba.core.sys.serialization.StandardSerialization

Used in all standard blocks.

cuba.springContextConfig

Additive property defining a spring.xml file in all standard application blocks.

The file is loaded using the Resources interface, so it can be located in classpath or in the configuration directory.

Used in all standard blocks.

Example:

cuba.springContextConfig = +com/company/sample/spring.xml
cuba.supportEmail

Specifies an email address to which exception reports from the default exception handler screen, as well as user messages from the Help > Feedback screen, will be sent.

Report button in the exception handler screen will be hidden if this property is set to an empty string.

In order to successfully send emails, the parameters described in Configuring Email Sending Parameters must also be configured.

Default value: empty string.

Stored in the database.

Interface: WebConfig

Used in the Web Client block.

cuba.tempDir

Defines the location of the temporary directory for an application block.

Default value for Fast Deployment in Tomcat: ${catalina.home}/temp/${cuba.webContextName}, which points to a subdirectory with the name of the web app inside the tomcat/temp folder, for example, tomcat/temp/app-core.

Default value for WAR and UberJAR deployment: ${app.home}/${cuba.webContextName}/temp, which points to a subdirectory of the application home.

Interface: GlobalConfig

Used in all standard blocks.

cuba.testMode

Must be set to true when the application is running automatic UI tests.

Interface: GlobalConfig

Default value: false

Used in Web Client and Middleware blocks.

cuba.themeConfig

Defines a set of *-theme.properties files that store theme variables, such as default popup window dimensions and input field width.

The property takes a list of files separated with spaces. The files are loaded as defined by the Resources interface.

Default value for Web Client: com/haulmont/cuba/havana-theme.properties com/haulmont/cuba/halo-theme.properties com/haulmont/cuba/hover-theme.properties

Used in the Web Client block.

cuba.triggerFilesCheck

Enables the processing of bean invocation trigger files.

The trigger file is a file that is placed in the triggers subdirectory of the application block’s temporary directory. The trigger file name consists of two parts separated with a period. The first part is the bean name, the second part is the method name of the bean to invoke. For example: cuba_Messages.clearCache. The trigger files handler monitors the folder for new trigger files, invokes the appropriate methods and then removes the files.

By default, the trigger files processing is configured in the cuba-web-spring.xml file and performed for the Web Client block only. At the project level, the processing for other modules can be performed by periodically invoking the process() method of the cuba_TriggerFilesProcessor bean.

Default value: true

Used in blocks with the configured processing, the default is Web Client.

cuba.triggerFilesCheckInterval

Defines the period in milliseconds of trigger files processing if the cuba.triggerFilesCheck is set to true.

Default value: 5000

Used in blocks with the configured processing, the default is Web Client.

cuba.trustedClientPassword

Defines password used to create TrustedClientCredentials. The Middleware layer can authenticate users who connect via the trusted client block without checking the user password.

This property is used when user passwords are not stored in the database, while the client block performs the actual authentication itself. For example, by integrating with Active Directory.

Interfaces: ServerConfig, WebAuthConfig, PortalConfig

Used in blocks: Middleware, Web Client, Web Portal.

cuba.trustedClientPermittedIpList

Defines the list of IP addresses, which is used with TrustedClientCredentials and TrustedClientService. For example:

cuba.trustedClientPermittedIpList = 127.0.0.1, 10.17.*.*

Default value: 127.0.0.1

Interfaces: ServerConfig

Used in the Middleware block.

cuba.uniqueConstraintViolationPattern

A regular expression which is used to find out that the exception is caused by a database unique constraint violation. The constraint name will be obtained from the first non-empty group of the expression. For example:

ERROR: duplicate key value violates unique constraint "(.+)"

The constraint name can be used to display a localized message that indicates what entity is concerned. For this, the main message pack should contain keys equal to constraint names. For example:

IDX_SEC_USER_UNIQ_LOGIN = A user with the same login already exists

This property allows you to define a reaction to unique constraint violations depending on DBMS locale and version.

Default value is returned by the PersistenceManagerService.getUniqueConstraintViolationPattern() method for the current DBMS.

Can be defined in the database.

Used in all client blocks.

cuba.useCurrentTxForConfigEntityLoad

Enables using current transaction, if there is one at the moment, for loading entity instances via the configuration interfaces. This could have a positive impact on performance. Otherwise, a new transaction is always created and committed, and the detached instances are returned.

Default value: false

Used in the Middleware block.

cuba.useEntityDataStoreForIdSequence

If the property is set to true, sequences for generating identifiers for BaseLongIdEntity and BaseIntegerIdEntity subclasses are created in the data store the entity belongs to. Otherwise, they are created in the main data store.

Default value: false

Interface: ServerConfig

Used in the Middleware block.

cuba.useInnerJoinOnClause

Indicates that EclipseLink ORM will use JOIN ON clause for inner joins instead of conditions in WHERE clause.

Default value: false

Used in the Middleware block.

cuba.useLocalServiceInvocation

When true, the Web Client and Web Portal blocks invoke the Middleware services locally bypassing the network stack, which has a positive impact on system performance. It is possible in the case of fast deployment, single WAR and single Uber JAR. This property should be set to false for all other deployment options.

Default value: true

Used in the Web Client and Web Portal blocks.

cuba.useReadOnlyTransactionForLoad

Indicates that all load methods of DataManager use read-only transactions.

Default value: true

Stored in the database.

Interface: ServerConfig

Used in the Middleware block.

cuba.user.fullNamePattern

Defines the full name pattern for user.

Default value: {FF| }{LL}

The full name pattern can be formed from the user’s first, last and middle names. The following rules apply to the pattern:

  • The pattern parts are separated with {}

  • The pattern inside {} must contain one of the following characters followed by the | character without any spaces:

    LL – long form of user’s last name (Smith)

    L – short form of user’s last name (S)

    FF – long form of user’s first name (John)

    F – short form of user’s first name (J)

    MM – long form of user’s middle name (Paul)

    M – short form of user’s middle name (P)

  • The | character can be followed by any symbols including spaces.

Used in the Web Client block.

cuba.user.namePattern

Defines the display name pattern for the User entity. The display name is used in different places, including the upper right corner of the system’s main window.

Default value: {1} [{0}]

{0} is substituted with the login attribute, {1} – with the name attribute.

Used in the Middleware and Web Client blocks.

cuba.userSessionExpirationTimeoutSec

Defines the user session expiration timeout in seconds.

Default value: 1800

Interface: ServerConfig

Used in the Middleware block.

It is recommended to use the same values for cuba.userSessionExpirationTimeoutSec and cuba.httpSessionExpirationTimeoutSec.

cuba.userSessionLogEnabled

Activates the user session log mechanism.

Default value: false

Stored in the database.

Interface: GlobalConfig.

Used in all standard blocks.

cuba.userSessionProviderUrl

Defines the Middleware block URL used for logging users in.

This parameter should be set in additional middleware blocks that execute client requests but do not share the user session cache. If there is no required session in the local cache at the start of the request, this block invokes the TrustedClientService.findSession() method at the specified URL, and caches the retrieved session.

Interface: ServerConfig

Used in the Middleware block.

cuba.viewsConfig

Additive property defining a views.xml file. See Views.

The file is loaded using the Resources interface, so it can be located in classpath or in the configuration directory.

Used in all standard blocks.

Example:

cuba.viewsConfig = +com/company/sample/views.xml
cuba.webAppUrl

Defines URL of the Web Client application.

In particular, used to generate external application screen links, as well as by the ScreenHistorySupport class.

Default value: http://localhost:8080/app

Stored in the database.

Interface: GlobalConfig

Can be used in all standard blocks.

cuba.windowConfig

Additive property defining a screens.xml file.

The file is loaded using the Resources interface, so it can be located in classpath or in the configuration directory.

Used in the Web Client block.

Example:

cuba.windowConfig = +com/company/sample/web-screens.xml
cuba.web.allowAnonymousAccess

Enables access to the application screens for non-authenticated users. If you set this property to true, make sure the Anonymous role is of Denying type, i.e anonymous users cannot open any screens by default.

Default value: false

Interface: WebConfig

Used in the Web Client block.

cuba.web.allowHandleBrowserHistoryBack

Enables handling of browser Back button in the application if the login and/or main window implements the CubaHistoryControl.HistoryBackHandler interface. If the property is true, the standard browser behavior is replaced with this method invocation.

Default value: true

Interface: WebConfig

Used in the Web Client block.

cuba.web.appFoldersRefreshPeriodSec

Defines application folders refresh period in seconds.

Default value: 180

Interface: WebConfig

Used in the Web Client block.

cuba.web.appWindowMode

Determines the initial mode for the main application window – "tabbed" or "single screen" (TABBED or SINGLE respectively). In the "single screen" mode, when a screen opens with the NEW_TAB parameter, it completely replaces the current screen instead of opening a new tab.

The user is able to change the mode later using the Help > Settings screen.

Default value: TABBED

Interface: WebConfig

Used in the Web Client block.

cuba.web.closeIdleHttpSessions

Defines whether the Web Client can close the UIs and the session when the session timeout is expired after the last non-heartbeat request.

Default value: false

Interface: WebConfig

Used in the Web Client block.

cuba.web.componentsConfig

Additive property defining a configuration file containing information about the application components supplied in separate jars or defined in cuba-ui-component.xml descriptor of web module.

For example:

cuba.web.componentsConfig =+demo-web-components.xml
cuba.web.customDeviceWidthForViewport

Defines custom viewport width for HTML page. Affects "viewport" meta tag of Vaadin HTML pages.

Default value: -1

Interface: WebConfig

Used in blocks: Web Client.

cuba.web.defaultScreenCanBeClosed

Defines whether the default screen can be closed by close button, ESC button or TabSheet context menu when TABBED work area mode is used.

Default value: true

Interface: WebConfig

Used in the Web Client block.

cuba.web.defaultScreenId

Defines the screen to be opened after login. This setting will be applied to all users.

For example:

cuba.web.defaultScreenId = sys$SendingMessage.browse

Interface: WebConfig

Used in the Web Client block.

cuba.web.foldersPaneDefaultWidth

Defines default width (in pixels) for the folders panel.

Default value: 200

Interface: WebConfig

Used in the Web Client block.

cuba.web.foldersPaneEnabled

Enables the folders panel functionality.

Default value: false

Interface: WebConfig

Used in the Web Client block.

cuba.web.foldersPaneVisibleByDefault

Determines whether the folders panel should be expanded by default.

Default value: false

Interface: WebConfig

Used in the Web Client block.

cuba.web.initialScreenId

Defines what screen will be open for non-authenticated users when they first open the application URL. Requires the cuba.web.allowAnonymousAccess to be set to true.

Interface: WebConfig

Used in the Web Client block.

cuba.web.ldap.enabled

Enables/disables LDAP login mechanism of the Web Client.

For example:

cuba.web.ldap.enabled = true

Interface: WebLdapConfig

Used in the Web Client block.

cuba.web.ldap.urls

Specifies LDAP server URLs.

For example:

cuba.web.ldap.urls = ldap://192.168.1.1:389

Interface: WebLdapConfig

Used in the Web Client block.

cuba.web.ldap.base

Specifies base DN for user search in LDAP.

For example:

cuba.web.ldap.base = ou=Employees,dc=mycompany,dc=com

Interface: WebLdapConfig

Used in the Web Client block.

cuba.web.ldap.user

The distinguished name of a system user which has the right to read the information from the directory.

For example:

cuba.web.ldap.user = cn=System User,ou=Employees,dc=mycompany,dc=com

Interface: WebLdapConfig

Used in the Web Client block.

cuba.web.ldap.password

The password for the system user defined in the cuba.web.ldap.user property.

For example:

cuba.web.ldap.password = system_user_password

Interface: WebLdapConfig

Used in the Web Client block.

cuba.web.ldap.userLoginField

The name of an LDAP user attribute that is used for matching the login name. sAMAccountName by default (suitable for Active Directory).

For example:

cuba.web.ldap.userLoginField = username

Interface: WebLdapConfig

Used in the Web Client block.

cuba.web.linkHandlerActions

Defines a list of URL commands handled by the LinkHandler bean. See Screen Links for more information.

The elements should be separated with the | character.

Default value: open|o

Interface: WebConfig

Used in the Web Client block.

cuba.web.loginDialogDefaultUser

Defines default user name, which will be automatically populated in the login screen. This is very convenient during development. This property should be set to <disabled> value in production environment.

Default value: admin

Interface: WebConfig

Used in the Web Client block.

cuba.web.loginDialogDefaultPassword

Defines default user password, which will be automatically populated in the login screen. This is very convenient during development. This property should be set to <disabled> value in production environment.

Default value: admin

Interface: WebConfig

Used in the Web Client block.

cuba.web.loginDialogPoweredByLinkVisible

Set to false to hide the "powered by CUBA Platform" link on the login dialog.

Default value: true

Interface: WebConfig

Used in the Web Client block.

cuba.web.loginScreenId

Identifier of a screen to be used as login screen of the application.

Default value: login

Interface: WebConfig

Used in the Web Client block.

cuba.web.mainScreenId

Identifier of a screen to be used as main screen of the application.

Default value: main

Interface: WebConfig

Used in the Web Client block.

cuba.web.mainTabSheetMode

Defines which component will be used for TABBED mode of main window. May have one of two possible string values from the MainTabSheetMode enumeration:

  • DEFAULT: CubaTabSheet component is used. It loads and unloads components each time the user switches tabs.

  • MANAGED: CubaManagedTabSheet is used. It doesn’t unload components from the tab when the user selects another tab.

Default value: DEFAULT.

Interface: WebConfig.

Used in the Web Client block.

cuba.web.managedMainTabSheetMode

If the cuba.web.mainTabSheetMode property is set to MANAGED, defines the way the managed main TabSheet switches its tabs: hides or unloads them.

Default value: HIDE_TABS

Interface: WebConfig

Used in the Web Client block.

cuba.web.maxTabCount

Defines the maximum number of tabs that can be opened in the main application window. The value of 0 disables this limitation.

Default value: 7

Interface: WebConfig

Used in the Web Client block.

cuba.web.pageInitialScale

Defines the initial scale of HTML page if cuba.web.customDeviceWidthForViewport is set or cuba.web.useDeviceWidthForViewport is true. Affects "viewport" meta tag of Vaadin HTML pages.

Default value: 0.8

Interface: WebConfig

Used in blocks: Web Client.

cuba.web.productionMode

Allows you to completely disable opening the Vaadin developer console in browser by adding ?debug to the application URL, and, therefore, disabling the JavaScript debug mode and reducing the amount of server information available from the browser.

Default value: false

Interface: WebConfig

Used in the Web Client block.

cuba.web.pushEnabled

Allows you to completely disable server push. The Background Tasks mechanism will not work in this case.

Default value: true

Interface: WebConfig

Used in Web Client.

cuba.web.pushLongPolling

Enables switching to long polling instead of WebSocket for server push implementation.

Default value: false

Interface: WebConfig

Used in Web Client.

cuba.web.pushLongPollingSuspendTimeoutMs

Defines push timeout in milliseconds, which is used in case of setting long polling instead of WebSocket for server push implementation, i.e. cuba.web.pushLongPolling="true".

Default value: -1

Interface: WebConfig

Used in Web Client.

cuba.web.rememberMeEnabled

Enables displaying Remember Me checkbox in the standard login screen of the web client.

Default value: true

Interface: WebConfig

Used in Web Client.

cuba.web.resourcesCacheTime

Enables configuring whether web resources should be cached or not. Value is set in seconds. Zero cache time disables caching at all. For example:

cuba.web.resourcesCacheTime = 136

Default value: 60 * 60 (1 hour).

Interface: WebConfig

Used in Web Client.

cuba.web.webJarResourcesCacheTime

Enables configuring whether WebJar resources should be cached or not. Value is set in seconds. Zero cache time disables caching at all. For example:

cuba.web.webJarResourcesCacheTime = 631

Default value: 60 * 60 * 24 * 365 (1 year).

Interface: WebConfig

Used in Web Client.

cuba.web.resourcesRoot

Sets a directory for loading files to display by Embedded component. For example:

cuba.web.resourcesRoot = ${cuba.confDir}/resources

Default value: null

Interface: WebConfig

Used in Web Client.

cuba.web.requirePasswordForNewUsers

If set to true then password is required on user creation from the Web Client. It is recommended to set value to false if you use LDAP authentication.

Default value: true

Interface: WebAuthConfig

Used in the Web Client block.

cuba.web.showBreadCrumbs

Enables hiding of the breadcrumbs panel which normally appears on top of the main window working area.

Default value: true

Interface: WebConfig

Used in the Web Client block.

cuba.web.showFolderIcons

Enables the folders panel icons. When enabled, the following application theme files are used:

  • icons/app-folder-small.png – for application folders.

  • icons/search-folder-small.png – for search folders.

  • icons/set-small.png – for record sets.

Default value: false

Interface: WebConfig

Used in the Web Client block.

cuba.web.standardAuthenticationUsers

A comma-separated list of users that are not allowed to use external authentication (such as LDAP or IDP SSO) and should log in to the system using standard authentication only.

An empty list means that everyone is allowed to login using external authentication.

Default value: <empty list>

Interface: WebAuthConfig

Used in the Web Client block.

cuba.web.table.cacheRate

Adjusts Table caching in the web browser. The amount of cached rows will be cacheRate multiplied with pageLength both below and above visible area.

Default value: 2

Interface: WebConfig

Used in the Web Client block.

cuba.web.table.pageLength

Sets the number of rows to be fetched from the server into the web browser when Table is rendered first time on refresh. See also cuba.web.table.cacheRate.

Default value: 15

Interface: WebConfig

Used in the Web Client block.

cuba.web.theme

Defines the name of the theme used as default for the web client. See also cuba.themeConfig.

Default value: halo

Interface: WebConfig

Used in the Web Client block.

cuba.web.uiHeartbeatIntervalSec

Defines the interval of the heartbeat requests for Web Client UI. If not set, the calculated value cuba.httpSessionExpirationTimeoutSec / 3 is used.

Default value: HTTP-session inactivity timeout, sec / 3

Interface: WebConfig

Used in the Web Client block.

cuba.web.unsupportedPagePath

Defines the path to the HTML page that is shown when an application doesn’t support the current browser version.

cuba.web.unsupportedPagePath = /com/company/sales/web/sys/unsupported-browser-page.html

Default value: /com/haulmont/cuba/web/sys/unsupported-page-template.html.

Interface: WebConfig.

Used in the Web Client block.

cuba.web.urlHandlingMode

Defines how URL changes should be handled.

Possible values are the elements of UrlHandlingMode enumeration:

Default value: URL_ROUTES.

Interface: WebConfig.

cuba.web.useFontIcons

If this property is enabled for Halo theme, Font Awesome glyphs will be used for standard actions and platform screens instead of images.

The correspondence between the name in the icon attribute of a visual component or action and font element is defined in the halo-theme.properties file of the platform. Keys with cuba.web.icons prefixes correspond to icon names, and their values - to com.vaadin.server.FontAwesome enumeration constants. For example, a font element for the standard create action is defined as follows:

cuba.web.icons.create.png = font-icon:FILE_O

Default value: true

Interface: WebConfig

Used in the Web Client block.

cuba.web.useInverseHeader

Controls the web client application header for Halo theme and its inheritors. If true, the header will be dark (inverse), if false - the header takes the colour of the main application background.

This property is ignored in case

$v-support-inverse-menu: false;

property is set in the application theme. This makes sense for a dark theme, if the user has the option to choose between a light and a dark theme. In this case, the header will be inverse for the light theme, and the same as the main background in the dark theme.

Default value: true

Interface: WebConfig

Used in the Web Client block.

cuba.web.userCanChooseDefaultScreen

Defines whether a user is able to choose the default screen. If the false value is set, the Default screen field in the Settings screen is read-only.

Default value: true

Interface: WebConfig

Used in the Web Client block.

cuba.web.useDeviceWidthForViewport

Handles the viewport width. Set true if device width should be used as viewport width. This property affects viewport meta tag of Vaadin HTML pages.

Default value: false

Interface: WebConfig

Used in blocks: Web Client.

cuba.web.viewFileExtensions

Defines a list of file extensions displayed in the browser when downloading the file using ExportDisplay.show(). The | character should be used to separate the list items.

Default value: htm|html|jpg|png|jpeg|pdf

Interface: WebConfig

Used in the Web Client block.

cuba.webContextName

Defines the web application context name. It is usually equivalent to the name of the directory or WAR-file containing this application block.

Interface: GlobalConfig

Used in blocks: Middleware, Web Client, Web Portal.

For example, for the Middleware block, located in tomcat/webapps/app-core and available at http://somehost:8080/app-core, the property should be set to the following value:

cuba.webContextName = app-core
cuba.webHostName

Defines the host name of the machine, on which this application block is running.

Default value: localhost

Interface: GlobalConfig

Used in blocks: Middleware, Web Client, Web Portal.

For example, for the Middleware block, available at http://somehost:8080/app-core, the property should be set to the following value:

cuba.webHostName = somehost
cuba.webPort

Defines the port, on which this application block is running.

Default value: 8080

Interface: GlobalConfig

Used in blocks: Middleware, Web Client, Web Portal.

For example, for the Middleware block, available at http://somehost:8080/app-core, this property should be set to the following value:

cuba.webPort = 8080

Appendix C: System Properties

System properties can be specified at JVM startup, using the command line argument -D. Additionally, system properties can be read or set using the getProperty() and setProperty() methods of the System class.

You can use system properties to set or override values of application properties. For example, the following command line argument will override the value of the cuba.connectionUrlList property which is normally set in the web-app.properties file:

-Dcuba.connectionUrlList=http://somehost:8080/app-core

Keep in mind, that system properties affect the whole JVM, i.e all application blocks running on the JVM will get the same value of a property.

System properties are cached by the framework at server startup, so your application should not rely on ability to override an application property by changing a system property at runtime. If you absolutely need it, reset the cache after changing a system property using the clearSystemPropertiesCache() methods of the CachingFacadeMBean JMX bean.

Below are the system properties that are used by the platform but are not application properties.

logback.configurationFile

Defines the location of the Logback framework configuration file.

For application blocks running on the Tomcat web server, this system property is configured in the tomcat/bin/setenv.bat and tomcat/bin/setenv.sh files. By default, it points to the tomcat/conf/logback.xml configuration file.

cuba.unitTestMode

This system property is set to true when the CubaTestCase base class is running integration tests.

Example:

if (!Boolean.valueOf(System.getProperty("cuba.unitTestMode")))
  return "Not in test mode";

Appendix D: Removed Sections

D.1. Organizing Business Logic

D.2. Business Logic in Controllers

D.3. Using Client Tier Beans

D.4. Using Middleware Services

D.5. Using Entity Listeners

See examples in the Entity Listeners section.

D.6. Using JMX Beans

See examples in the Creating a JMX Bean section.

D.7. Running Code on Startup

See the example in the Registration of entity listeners section.

D.8. Working with Generic UI

D.9. Themes in Web Applications

See Themes.

D.10. Migration from Havana to Feature-rich Halo Theme

See the example in Modifying common theme parameters section.

D.11. Passing Parameters to a Screen

D.12. Returning Values from an Invoked Screen

D.13. Using Individual Fields instead of FieldGroup

TODO

D.14. Setting up Logging in The Desktop Client

Not relevant since version 7.0 as the desktop client is not supported anymore.

D.16. Many-to-Many Associations

D.17. Direct Many-to-Many Association

D.18. Many-to-Many Association with Link Entity

D.19. Entity Inheritance

D.20. Composite Structures

D.21. One-to-Many: One Level of Nesting

D.22. One-to-Many: Two Levels of Nesting

D.23. One-to-Many: Three Levels of Nesting

D.24. One-to-One Composition

D.25. One-to-One Composition with a Single Editor

D.26. Assigning Initial Values

D.27. Entity Fields Initialization

D.28. Initialization Using CreateAction

D.29. Using initNewItem Method

D.30. Getting Localized Messages

D.31. Main Window Layout

D.32. REST API

REST API has been moved to add-on, see its documentation.

D.33. Working with Databases

D.34. Creating the Database Schema

D.35. Displaying Images in a Table Column

See Working with Images guide.

D.36. Loading and Displaying Images

See Working with Images guide.

D.37. Cookbook

See Guides.

7. Glossary

Application Tiers

See Application Tiers and Blocks.

Application Properties

Application properties are named data values of various types that define different aspects of application configuration or functions. See Application Properties.

Application Blocks

See Application Tiers and Blocks.

Artifact

In the context of this manual, an artifact is a file (usually a JAR or ZIP file) that contains executable code or other code obtained as a result of building a project. An artifact has a name and a version number defined according to specific rules and can be stored in the artifact repository.

Artifact Repository

A server that stores  artifacts  in a specific structure. The artifacts that the project depends on are loaded from the repository when that project is built.

Base Projects

The same as application components. This term was used in the previous versions of the platform and documentation.

Container

Containers control lifecycle and configuration of application objects. This is a base component of the dependency injection mechanism also known as Inversion of Control.

CUBA platform uses the Spring Framework container.

DB

A relational database.

Dependency Injection

Also known as Inversion of Control (IoC) principle. A mechanism for retrieving links to the objects being used, which assumes that an object should only declare which objects it depends on, while the container creates all the necessary objects and injects them in the dependent object.

Eager Fetching

Loading data from subclasses and related objects together with the requested entity.

Entity

Main element of the data model, see Data Model.

Entity Browser

A screen containing a table with a list of entities and buttons to create, edit and delete entities.

EntityManager

A middle tier component for working with persistent  entities.

Interceptor

An element of aspect-oriented programming that enables changing or extending object method invocations.

JMX

Java Management Extensions − a technology that provides tools to manage applications, system objects and devices. Defines the standard for JMX-components.

See also Using JMX Tools.

JPA

Java Persistence API – a standard specification of the object-relational mapping technology (ORM). CUBA platform uses  EclipseLink framework that implements this specification.

JPQL

Database-independent object-oriented query language, defined as a part of the JPA specification. See https://en.wikibooks.org/wiki/Java_Persistence/JPQL.

Lazy loading

See Lazy Loading.

Local attribute

An entity attribute that is not a reference or a collection of references to other entities. Values of all local entity attributes are typically stored in one table (with the exception of certain entity inheritance strategies).

Localized message pack

See Message Packs.

Managed Beans

 Components that contain application business logic.

Main Message Pack

See Main Message Pack.

MBeans

Managed Beans that have a JMX-interface. Typically, such beans have an internal state (e.g. cache, configuration data or statistics) that needs to be accessible through JMX.

Middleware

Middle tier –  the application tier  that contains the business logic, works with the database and provides a common interface for higher client tier of an application.

Optimistic locking

Optimistic locking – an approach to managing access to shared data by different users that assumes a very low probability of simultaneous access to the same entity instance. With this approach, locking itself is not applied, instead the system checks if a newer version of the data is available in the database at the moment when the changes are being saved. If so, an exception is thrown and the user must reload the entity instance.

ORM

Object-Relational Mapping – a technology that links tables in a relational database to objects of a programming language.

See ORM Layer.

Persistent context

A set of entity instances loaded from the database or just created. Persistent context serves as data cache within the current transaction. When transaction is committed, all persistent context entity changes are saved to a database.

Screen Controller

A Java class containing screen initialization and event handling logic. Works in conjunction with screen’s XML-descriptor.

Services

Middleware services provide the business interface for client calls and form the Middleware boundary. Services can encapsulate the business logic or delegate the execution to Managed Beans.

See Services.

Soft deletion

See Soft Deletion.

UI

User Interface.

View

See Views

XML-descriptor

An XML file containing layout of visual and data components of a screen.

. . .