A new generation of the platform is Jmix.

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.2 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.2), then we recommend using Java 11.

    Pay attention that OpenJ9 JVM is not supported.

  • 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

Visit Quick Start page on the platform website to learn the main things for creating any web application: how to create a project and database, how to design data model and how to create user interface. Make sure that the necessary software is already installed and set up on your computer, see Setup.

You can go further in studying CUBA with help of training examples on the Learn page on the CUBA Platform website.

The next essential step of developing can be putting business logic into your application. See the following guide:

To design more complicated data model see the following guides:

Get an overview on how to read and write data in CUBA applications with the help of this guide:

The more information on how to work with events, localize messages, provide user access and test your application you can find on the Guides page.

The most code examples in this manual are based on the data model used in Sales Application.

More Information

VIDEOS

Tutorials and webinars that explain concepts and techniques in detail

GUIDES

Let you be guided through different aspects of the platform

LIVE DEMO

Live applications demonstrating Platform functionality

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 1. 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. Unlike Web Portal, it is a client-side application (for example, a JavaScript application executed in the web browser). It communicates with the middleware via REST API running either in Web Client or in Web Portal blocks. It can be powered by React or other libraries/frameworks. 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 2. 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 3. 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.

See our guides to learn about data modeling in CUBA applications. Data Modelling: Many-to-Many Association guide shows different use cases of many to many associations. Data Modelling: Composition guide shows various examples of a composition relationship between entities.

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.

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.

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

Data Modelling: Entity Inheritance guide shows the mechanism to define entity inheritance.

@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|amount,customer")
...
public String getCaption(){
    String prefix = "";
    if (amount > 5000) {
        prefix = "Grade 1 ";
    } else {
        prefix = "Grade 2 ";
    }
    return prefix + customer.name;
}
@PostConstruct

This annotation can be specified for a method. The annotated method will be invoked right after the entity instance is created by the Metadata.create() method or similar DataManager.create() and DataContext.create() methods.

See Initial Entity Values guide for an example of defining initial values directly in the entity class using the @PostConstruct annotation.

The annotated method can accept Spring beans available in the global module as parameters. For example:

@PostConstruct
public void postConstruct(Metadata metadata, SomeBean someBean) {
    // ...
}
@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.

Data Modelling: Composition guide shows various examples of a composition relationship between entities.

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.

@CurrencyValue

Indicates that a field which is marked by this annotation contains currency value. If used, the CurrencyField is generated by Studio for this attribute in a Form of the entity editor screen.

Parameters:

  • currency – currency name: USD, GBP, EUR, $, or another currency sign.

  • labelPosition - currency label position: RIGHT (default), LEFT.

Example:

@CurrencyValue(currency = "$", labelPosition = CurrencyLabelPosition.LEFT)
@Column(name = "PRICE")
protected BigDecimal price;
@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;
@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.

Data Modelling: Many-to-Many Association guide shows different use cases of many to many associations.

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. Also, if the annotation is applied to a getter method, i.e. the entity attribute is read-only, changes of related attributes generate PropertyChangeEvent for the given read-only attribute. This feature helps to update UI components displaying read-only attributes that depend on some other mutable attributes.

Field example:

@Transient
@MetaProperty
protected String token;

Method example:

@MetaProperty(related = "firstName,lastName")
public String getFullName() {
    return firstName + " " + lastName;
}
@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 4. 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. They follow the logic of standard Java SE classes, such as DecimalFormat (see https://docs.oracle.com/javase/tutorial/i18n/format/decimalFormat.html) or SimpleDateFormat (see https://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html).

The formats should be provided in the strings with the following keys:

  • numberDecimalSeparator – decimal separator for numeric types.

    # comma as decimal separator
    numberDecimalSeparator=,
  • numberGroupingSeparator – thousands separator for numeric types.

    # space as thousands grouping separator
    numberGroupingSeparator = \u0020
  • integerFormat – format for Integer and Long types.

    # disable thousands grouping separator for integers
    integerFormat = #0
  • doubleFormat – format for Double type. Note that symbols used for decimal and grouping separators are defined in their own keys shown above.

    # rounding up to three digits after decimal separator
    doubleFormat=#,##0.###
  • decimalFormat – format for BigDecimal type. Note that symbols used for decimal and grouping separators are defined in their own keys shown above.

    # always display two digits after decimal separator, e.g. for currencies
    decimalFormat = #,##0.00
  • dateTimeFormat – format for java.util.Date type.

    # Russia date+time format
    dateTimeFormat = dd.MM.yyyy HH:mm
  • dateFormat – format for java.sql.Date type.

    # United States date format
    dateFormat = MM/dd/yyyy
  • timeFormat – format for java.sql.Time type.

    # hours:minutes time format
    timeFormat=HH:mm
  • offsetDateTimeFormat – format for java.time.OffsetDateTime type.

    # date+time format with an offset from GMT
    offsetDateTimeFormat = dd/MM/yyyy HH:mm Z
  • offsetTimeFormat – format for java.time.OffsetTime type.

    # hours:minutes time format with an offset from GMT
    offsetTimeFormat=HH:mm Z
  • trueString – string corresponding to Boolean.TRUE.

    # alternative displaying for boolean values
    trueString = yes
  • falseString – string corresponding to Boolean.FALSE.

    # alternative displaying for boolean values
    falseString = no

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

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.

See the next section for how to define views.

Below some internals of the views mechanism are explained.

View
Figure 5. 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.

3.2.3.1. Creating Views

You can create views as follows:

  • Programmatically – by creating a View instance. This way is appropriate for creating views that are used in the business logic.

    View instances, including nested ones, can be created by constructors:

    View view = new View(Order.class)
            .addProperty("date")
            .addProperty("amount")
            .addProperty("customer", new View(Customer.class)
                .addProperty("name")
            );

    The same can be done using ViewBuilder:

    View view = ViewBuilder.of(Order.class)
            .addAll("date", "amount", "customer.name")
            .build();

    ViewBuilder can also be used in DataManager's fluent interface:

    // explicit view builder
    dataManager.load(Order.class)
            .view(viewBuilder ->
                viewBuilder.addAll("date", "amount", "customer.name"))
            .list();
    
    // implicit view builder
    dataManager.load(Order.class)
            .viewProperties("date", "amount", "customer.name")
            .list();
  • Declaratively in screens - by defining an inline view right in the screen’s XML, see an example in Declarative Creation of Data Components. This is the recommended way for loading data in Generic UI screens for projects based on CUBA 7.2+.

  • Declaratively in the shared repository – by defining a view in the views.xml file of the project. The views.xml file is deployed on the application start, created View instances are cached in ViewRepository. 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.

Below is an example of the XML view declaration which provides loading of all local attributes of the Order entity, 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="item-in-order"/>
</view>
Working with the shared views repository

ViewRepository is a Spring bean, accessible to all application blocks. The reference to ViewRepository can be obtained using injection or through the AppBeans static methods. Use ViewRepository.getView() methods to retrieve view instances from 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).

CUBA Studio automatically creates and maintains a single views.xml file in your project. However, you can create multiple view descriptor files as follows:

  • The global module must contain the views.xml file with all view descriptors that should be globally accessible (i.e. on all application tiers) into it. This file should be registered 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 is what Studio does for you by default.

  • If you have a large number of shared views, you can put them in multiple files, e.g. into standard views.xml and additional foo-views.xml, bar-views.xml. Register all your files in the cuba.viewsConfig application property:

    cuba.viewsConfig = +com/company/sales/views.xml com/company/sales/foo-views.xml com/company/sales/bar-views.xml
  • If there are views which are used only in one application block, they can be defined 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 shared views. For example, not just "browse", but "customer-browse". It simplifies the search of views in XML descriptors.

3.2.4. Spring Beans

Spring Beans are classes that are instantiated and populated with dependencies by the Spring Framework container. Beans are designed for implementation of the application’s business logic.

Spring 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 its fields (in other words, has a state), it is necessary to synchronize access to these fields.

3.2.4.1. Creating a Bean

See Create Business Logic in CUBA guide to learn how to put business logic as a Spring bean.

To create a Spring 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 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 bean class should be placed inside the package tree with the root specified in the context:component-scan element of the spring.xml file. For the bean in the example above, the spring.xml file should contain 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.

Spring beans can be created on any application tier.

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’s 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 Spring 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 6. 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 Spring 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 Spring 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 beans, so they can be injected into any other managed components (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 Introduction to Working with Data guide to learn different use cases regarding programmatic data access with the DataManager API.

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.

The query() method of the fluent interface accepts both full and abbreviated query string. The latter should be created in accordance with the following rules:

  • You can always omit the "select <alias>" clause.

  • If the "from" clause contains a single entity and you don’t need a specific alias for it, you can omit the "from <entity> <alias> where" clause. In this case, the framework assumes that the alias is e.

  • You can use positional parameters and pass their values right to the query() method as additional arguments.

For example:

// named parameter
dataManager.load(Customer.class)
        .query("e.name like :name")
        .parameter("name", value)
        .list();

// positional parameter
dataManager.load(Customer.class)
        .query("e.name like ?1", value)
        .list();

// case-insensitive positional parameter
dataManager.load(Customer.class)
        .query("e.name like ?1 or e.email like ?1", "(?i)%joe%")
        .list();

// multiple positional parameters
dataManager.load(Order.class)
        .query("e.date between ?1 and ?2", date1, date2)
        .list();

// omitting "select" and using a positional parameter
dataManager.load(Order.class)
        .query("from sales_Order o, sales_OrderLine l " +
                "where l.order = o and l.product.name = ?1", productName)
        .list();

Please note that positional parameters are supported only in the fluent interface. LoadContext.Query supports only named parameters.

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

See Decouple Business Logic with Application Events guide to learn different examples of application 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 Spring 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.

  • 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 Spring 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, via Java system properties and OS environment variables. If a property with the same name is defined in different sources, its value is determined by the following priorities:

  1. Java system property (highest priority)

  2. OS environment variable

  3. Properties file

  4. Database (lowest priority)

For example, a value specified in a properties file overrides the value specified in the database.

For OS environment variables, the framework tries to find the exact match of property name, and if not found, it looks for a name converted to upper case and with dots replaced with underscores. For example, MYAPP_SOMEPROPERTY environment variable can assign value to myapp.someProperty application property. If you want to disable this feature and keep only exact match, set the cuba.disableUppercaseEnvironmentProperties application property to true.

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 and OS environment variables. 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 in an external local.app.properties file. 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:${app.home}/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: here app.home system property points to the application home directory.

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. If any of these files does not exist, it is silently ignored.

The last file in the above set is local.app.properties. It can be used to set or override application properties specific to a particular deployment environment.

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, via Java system property or OS environment variable.

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

  1. 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);
    }
  2. There is no need to create TypeStringify as toString() method is sufficient in this case.

  3. Annotate the property in the configuration interface:

    @Factory(factory = UuidTypeFactory.class)
    UUID getUuidProp();
    void setUuidProp(UUID value);

The platform provides TypeFactory and Stringify implementations for the following types:

  • UUIDUuidTypeFactory, as described above. TypeStringify is redundant here, you can easily use toString() method for UUID.

  • java.util.DateDateFactory and DateStringify. 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

    Example of describing a date property in the configuration interface:

    @Property("cuba.test.dateProp")
    @Factory(factory = DateFactory.class)
    @Stringify(stringify = DateStringify.class)
    Date getDateProp();
    
    void setDateProp(Date date);
  • List<Integer> (the list of integers) – IntegerListTypeFactory and IntegerListStringify. The property value must be specified in the form of numbers, separated by spaces, for example:

    cuba.test.integerListProp = 1 2 3

    Example of describing a List<Integer> property in the configuration interface:

    @Property("cuba.test.integerListProp")
    @Factory(factory = IntegerListTypeFactory.class)
    @Stringify(stringify = IntegerListStringify.class)
    List<Integer> getIntegerListProp();
    
    void setIntegerListProp(List<Integer> list);
  • List<String> (the list of strings) – StringListTypeFactory and StringListStringify. The property value must be specified as a list of strings separated by "|" sign, for example:

    cuba.test.stringListProp = aaa|bbb|ccc

    Example of describing a List<String> property in the configuration interface:

    @Property("cuba.test.stringListProp")
    @Factory(factory = StringListTypeFactory.class)
    @Stringify(stringify = StringListStringify.class)
    List<String> getStringListProp();
    
    void setStringListProp(List<String> list);
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.

See Localization in CUBA applications guide to learn how localized messages within the CUBA application can be defined and used.

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.

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

Messages about database unique constraint violation should contain keys equal to constraint names and be in uppercase:

IDX_SEC_USER_UNIQ_LOGIN = A user with the same login already exists
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.

See Anonymous Access & Social Login guide to learn how to set up public access to some screens of the application and implement custom login using a Google, Facebook, or GitHub account.

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 7. 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 8. 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.getUserSession().getUser();
        log.info("Logged in user {}", user.getLogin());
    }
}

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 Spring 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.12.3.1. Handling Unique Constraint Violation Exceptions

The framework allows you to customize the message displayed by an exception handler for a database constraint violation error.

A custom message should be specified in the main message pack of the web module with a key equal to the database unique index name in uppercase. For example:

IDX_SEC_USER_UNIQ_LOGIN = A user with the same login already exists

So, for example, if you got this notification:

unique constraint message

then by adding

IDX_DEMO_PRODUCT_UNIQ_NAME = A product with this name already exists

to the main message pack, you will get the following notification:

unique constraint message 2

Detection of database constraint violation errors is done by the UniqueConstraintViolationHandler class which uses regular expressions depending on your database type. If the default expression doesn’t recognize errors from your database, try to adjust it using the cuba.uniqueConstraintViolationPattern application property.

You can also completely replace the standard handler by providing your own exception handler with a higher precedence, e.g. @Order(HIGHEST_PLATFORM_PRECEDENCE - 10).

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 setCrossFieldValidate() method of the controller:

public class EventEdit extends StandardEditor<Event> {
    @Subscribe
    public 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);
    // ...
}

See the Validation in Java applications article in our blog for more details. The sample application described in this article is available on GitHub.

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 Spring 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 Spring 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. Databases

This section provides information on how to configure database connections and work with particular types of DBMS. It also describes a database migration 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-related components belong to the Middleware block; other blocks of the application do not have direct access to the database.

3.3.1. Connecting to Databases

CUBA application obtains connections to a database through JDBC DataSource. A data source can be configured in the application or obtained from JNDI. The method of obtaining the data source is specified by the cuba.dataSourceProvider application property: it can be either application or jndi.

You can easily configure connections for the main and additional data stores using CUBA Studio, see its documentation. The information below can be helpful for troubleshooting and for defining parameters not available in Studio, e.g. connection pool settings.

Configuring a Data Source in the Application

When the data source is configured in the application, the framework creates a connection pool using HikariCP. Both the connection parameters and the pool settings are configured using application properties located in the app.properties file of the core module. This is the recommended way if you don’t need a specific connection pool provided by an application server.

The following application properties define the database type and connection parameters:

  • cuba.dbmsType - defines the DBMS type.

  • cuba.dataSourceProvider - application value indicates that the data source must be configured using application properties.

  • cuba.dataSource.username - the database user name.

  • cuba.dataSource.password - the database user password.

  • cuba.dataSource.dbName - the database name.

  • cuba.dataSource.host - the database host.

  • cuba.dataSource.port - optional parameter, sets the database port if it is non-standard for the selected DBMS type.

  • cuba.dataSource.jdbcUrl - optional parameter, sets the full JDBC URL if some additional connection parameters need to be passed. Note that all separate properties described above are still required for database migration tasks.

In order to configure connection pool settings, specify the HikariCP properties prefixed with cuba.dataSource., for example cuba.dataSource.maximumPoolSize or cuba.dataSource.connectionTimeout. See the full list of supported parameters and their default values in the HikariCP documentation.

If your application uses additional data stores, you should define the same set of parameters for each data store. The data store name is added to the second part of each property name:

For example:

# main data store connection parameters
cuba.dbmsType = hsql
cuba.dataSourceProvider = application
cuba.dataSource.username = sa
cuba.dataSource.password =
cuba.dataSource.dbName = demo
cuba.dataSource.host = localhost
cuba.dataSource.port = 9111
cuba.dataSource.maximumPoolSize = 20

# names of additional data stores
cuba.additionalStores = clients,orders

# 'clients' data store connection parameters
cuba.dbmsType_clients = postgres
cuba.dataSourceProvider_clients = application
cuba.dataSource_clients.username = postgres
cuba.dataSource_clients.password = postgres
cuba.dataSource_clients.dbName = clients_db
cuba.dataSource_clients.host = localhost

# 'orders' data store connection parameters
cuba.dbmsType_orders = mssql
cuba.dataSourceProvider_orders = application
cuba.dataSource_orders.jdbcUrl = jdbc:sqlserver://localhost;databaseName=orders_db;currentSchema=my_schema
cuba.dataSource_orders.username = sa
cuba.dataSource_orders.password = myPass123
cuba.dataSource_orders.dbName = orders_db
cuba.dataSource_orders.host = localhost

Besides, for each additional data store, the spring.xml file of the core module must contain a definition of the CubaDataSourceFactoryBean bean with the appropriate storeName parameter. For example:

<bean id="cubaDataSource_clients" class="com.haulmont.cuba.core.sys.CubaDataSourceFactoryBean">
    <property name="storeName" value="clients"/>
</bean>

<bean id="cubaDataSource_orders" class="com.haulmont.cuba.core.sys.CubaDataSourceFactoryBean">
    <property name="storeName" value="orders"/>
</bean>

If you configure the data source in the application, the database migration Gradle tasks may have no parameters, as they will be obtained from the same set of application properties. This is an additional benefit of configuring data sources in the application. For example:

task createDb(dependsOn: assembleDbScripts, description: 'Creates local database', type: CubaDbCreation) {
}

task updateDb(dependsOn: assembleDbScripts, description: 'Updates local database', type: CubaDbUpdate) {
}
Obtaining a Data Source from JNDI

If you want to use a data source provided by an application server via JNDI, define the following application properties in the app.properties file of the core module:

  • cuba.dbmsType - defines the DBMS type.

  • cuba.dataSourceProvider - jndi value indicates that the data source must be obtained from JNDI.

The JNDI name of the data source is specified in the cuba.dataSourceJndiName application property, which is java:comp/env/jdbc/CubaDS by default. For additional data stores, specify the same properties adding the data store name.

For example:

# main data store connection parameters
cuba.dbmsType = hsql
cuba.dataSourceProvider = jndi

# names of additional data stores
cuba.additionalStores = clients,orders

# 'clients' data store connection parameters
cuba.dbmsType_clients = postgres
cuba.dataSourceProvider_clients = jndi
cuba.dataSourceJndiName_clients = jdbc/ClientsDS

# 'orders' data store connection parameters
cuba.dbmsType_orders = mssql
cuba.dataSourceProvider_orders = jndi
cuba.dataSourceJndiName_orders = jdbc/OrdersDS

Besides, for each additional data store, the spring.xml file of the core module must contain a definition of the CubaDataSourceFactoryBean bean with the appropriate storeName and jndiNameAppProperty parameters. For example:

<bean id="cubaDataSource_clients" class="com.haulmont.cuba.core.sys.CubaDataSourceFactoryBean">
    <property name="storeName" value="clients"/>
    <property name="jndiNameAppProperty" value="cuba.dataSourceJndiName_clients"/>
</bean>

<bean id="cubaDataSource_orders" class="com.haulmont.cuba.core.sys.CubaDataSourceFactoryBean">
    <property name="storeName" value="orders"/>
    <property name="jndiNameAppProperty" value="cuba.dataSourceJndiName_orders"/>
</bean>

Data sources provided via JNDI are configured in a way specific to the application server. In Tomcat, it is done in the context.xml file. CUBA Studio writes connection parameters into modules/core/web/META-INF/context.xml and use this file in the standard deployment process when developing the application.

If the data source is configured in context.xml, the database migration Gradle tasks must have own parameters defining the database connection, for example:

task createDb(dependsOn: assembleDbScripts, description: 'Creates local database', type: CubaDbCreation) {
    dbms = 'hsql'
    host = 'localhost:9111'
    dbName = 'demo'
    dbUser = 'sa'
    dbPassword = ''
}

task updateDb(dependsOn: assembleDbScripts, description: 'Updates local database', type: CubaDbUpdate) {
    dbms = 'hsql'
    host = 'localhost:9111'
    dbName = 'demo'
    dbUser = 'sa'
    dbPassword = ''
}
3.3.1.1. 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

If you are using Studio, add the currentSchema connection parameter to the Connection params field in the Data Store Properties window. Studio will automatically update project files according to your data source configuration method. Otherwise, specify the connection parameter manually as described below.

If you configure the data source in the application, add the full URL property, for example:

cuba.dataSource.jdbcUrl = jdbc:postgresql://localhost/demo?currentSchema=my_schema

If you obtain the data source from JNDI, add the currentSchema parameter to the connection URL in the data source definition (for Tomcat it is in context.xml) and to the connectionParams property of the createDb and updateDb Gradle tasks, for example:

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

Now 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'

If you are using Studio, add the currentSchema connection parameter to the Connection params field in the Data Store Properties window. Studio will automatically update project files according to your data source configuration method. Otherwise, specify the connection parameter manually as described below.

If you configure the data source in the application, add the full URL property, for example:

cuba.dataSource.jdbcUrl = jdbc:sqlserver://localhost;databaseName=demo;currentSchema=my_schema

If you obtain the data source from JNDI, add the currentSchema parameter to the connection URL in the data source definition (for Tomcat it is in context.xml) and to the connectionParams property of the createDb and updateDb Gradle tasks.

task updateDb(dependsOn: assembleDbScripts, type: CubaDbUpdate) {
    dbms = 'mssql'
    dbmsVersion = '2012'
    host = 'localhost'
    dbName = 'demo'
    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.2. 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 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

MariaDB 5.5+

mysql

org.mariadb.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 MariaDB

UUID

varchar(36)

uuid

uniqueidentifier

varchar2(32)

varchar(32)

varchar(32)

Date

timestamp

timestamp

datetime

timestamp

datetime(3)

datetime(3)

java.sql.Date

timestamp

date

datetime

date

date

date

java.sql.Time

timestamp

time

datetime

timestamp

time(3)

time(3)

BigDecimal

decimal(p, s)

decimal(p, s)

decimal(p, s)

number(p, s)

decimal(p, s)

decimal(p, s)

Double

double precision

double precision

double precision

float

double precision

double precision

Long

bigint

bigint

bigint

number(19)

bigint

bigint

Integer

integer

integer

integer

integer

integer

integer

Boolean

boolean

boolean

tinyint

char(1)

boolean

boolean

String (limited)

varchar(n)

varchar(n)

varchar(n)

varchar2(n)

varchar(n)

varchar(n)

String (unlimited)

longvarchar

text

varchar(max)

clob

longtext

longtext

byte[]

longvarbinary

bytea

image

blob

longblob

longblob

Usually, all the 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. It means that no manual conversion is required when working with the data using DataManager, EntityManager 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 the DBMS in use. 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.2.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.

  • If you configure the data source in the application, specify the full connection URL in the required format as described in the Connecting to Databases section.

  • 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'
    }

It is also possible to add support for the custom database into the CUBA Studio. When integration is implemented, Studio allows developer to use standard dialogs when changing data store settings. Also (the most important) Studio will be able to automatically generate database migration scripts for entities defined in the platform, add-ons and the project. Integration instructions are available in the corresponding section of the Studio User Guide.

A working example of integrating a custom database (Firebird) into the CUBA platform and CUBA Studio can be found here: https://github.com/cuba-labs/firebird-sample.

3.3.2.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.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.2.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 setting the following parameters in the Connection params field of the Data Store Properties window:

?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.3. Database Migration Scripts

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.

CUBA Studio automatically creates database migration scripts for your changing data model, see its documentation. The information below can be helpful for better understanding of the process and for creating Groovy-based migration scripts not supported in Studio.

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 in Column definition entity attributes and sqlType of custom datatypes. If you changed them, create appropriate update scripts manually.

The create scripts for the main data store are located in the /db/init directory of the core module. Create scripts for additional data stores (if any) must be located in /db/init_<datastore_name> directory. 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 for the main data store are located in the /db/update directory of the core module. Update scripts for additional data stores (if any) must be located in /db/update_<datastore_name> directory. 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.3.1. The Structure of SQL Scripts

Database migration 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.3.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, keep in mind that no beans, infrastructure interfaces and other application objects have been instantiated yet and it is impossible to use them.

    The main part of the script 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.3.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 defined in the build.gradle build script. It can be done from the command line or via the Studio interface.

To run scripts to create the database, use the createDb task. 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, use the updateDb task. 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.3.4. Execution of Database Scripts by Server

The mechanism of executing 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 – e.g. a production or developer’s Tomcat instance.

Depending on the conditions described below, this mechanism executes either create or update scripts, i.e., it can both initialize the database 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 database automatically but only executes scripts on it.

The mechanism to execute scripts by the server works as follows:

  • The application reads scripts from the database scripts directory defined by the cuba.dbDir application property, which is set by default to WEB-INF/db.

  • If the database does not have the SEC_USER table, it is considered empty and the full initialization is performed 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 database 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 database has both the SEC_USER and SYS_DB_CHANGELOG tables, the update scripts which 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 an already existing database 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 usually required.

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 9. Components of the Middle Tier

Services are Spring beans 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 Spring beans 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 Spring 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

See Create Business Logic in CUBA guide to learn how to put business logic as a middleware 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 Spring 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 Spring 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. An additional data store can be any implementation of the DataStore interface.

CUBA Studio allows you to set up additional data stores, see the documentation. It automatically creates all required application properties and JDBC data sources, as well as maintains additional persistence.xml files. After setting up a data store, you can select it 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.

The information below can help you in troubleshooting or if you don’t use Studio.

Additional data store names are specified in the cuba.additionalStores application property. If the additional store is RdbmsStore, the following properties should be defined for it:

  • cuba.dbmsType_<store_name> - type of the data store DBMS.

  • cuba.persistenceConfig_<store_name> - location of the data store persistence.xml file.

  • cuba.dataSource…​ - connection parameters as described in Connecting to Databases.

If you implement the DataStore interface in your project, the name of the implementation bean must be specified in the cuba.storeImpl_<store_name> application property.

For example, if you work with two additional data stores: db1 (a PostgreSQL database) and mem1 (a custom in-memory storage implemented by some project bean), the following application properties must be specified in the app.properties file of your core module:

cuba.additionalStores = db1, mem1

# RdbmsStore for Postgres database with data source obtained from JNDI
cuba.dbmsType_db1 = postgres
cuba.persistenceConfig_db1 = com/company/sample/db1-persistence.xml
cuba.dataSourceJndiName_db1 = jdbc/db1

# Custom store
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.).

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

A Spring 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

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 Spring 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 Spring 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 Spring 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

See Decouple Business Logic with Application Events guide to learn how to use 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 (using @EventListener) 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.TransactionalEventListener;
import java.util.UUID;

@Component("demo_CustomerChangedListener")
public class CustomerChangedListener {

    @EventListener (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 (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 (with @EventListener or @TransactionalEventListener(phase = 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 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;

    @EventListener
    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 10. 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 11. 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 12. 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 Controller Methods

In this section, we describe some methods of screen controller base classes that can be invoked or overridden in the application code.

Methods of all screens
  • show() - shows the screen. This method is usually used after creating the screen as described in the Opening Screens section.

  • close() - closes the screen with the passed StandardOutcome enum value or a CloseAction object. For example:

    @Subscribe("closeBtn")
    public void onCloseBtnClick(Button.ClickEvent event) {
        close(StandardOutcome.CLOSE);
    }

    The parameter value is propagated to BeforeCloseEvent and AfterCloseEvent, so the information about the reason why the screen was closed can be obtained in the listeners. For more information on using these listeners see Executing code after close and returning values.

  • getScreenData() - returns the ScreenData object that serves as a registry for all data components defined in the screen XML descriptor. You can use its loadAll() method to trigger all data loaders of the screen:

    @Subscribe
    public void onBeforeShow(BeforeShowEvent event) {
        getScreenData().loadAll();
    }
  • getSettings() - returns the Settings object that can be used to read or write custom settings associated with the screen for the current user.

  • saveSettings() - saves the screen settings represented by the Settings object. This method is called automatically if cuba.gui.manualScreenSettingsSaving application property is set to false (which is the default).

Methods of StandardEditor
  • getEditedEntity() - when the screen is shown, returns an instance of the entity being edited. It’s the instance which is set in the data container specified in the @EditedEntityContainer annotation.

    In InitEvent and AfterInitEvent listeners, this method returns null. In BeforeShowEvent listener, this method returns the instance passed to the screen for editing (later in the screen opening process the entity is reloaded and a different instance is set to the data container).

The following methods can be used to close the edit screen:

  • closeWithCommit() - validates and saves data, then closes the screen with StandardOutcome.COMMIT.

  • closeWithDiscard() - ignores any unsaved changes and closes the screen with StandardOutcome.DISCARD.

If the screen has unsaved changes in DataContext, a dialog with a corresponding message will be displayed before the screen is closed. You can adjust the notification type using the cuba.gui.useSaveConfirmation application property. If you use the closeWithDiscard() or close(StandardOutcome.DISCARD) methods, unsaved changes are ignored without any message.

  • commitChanges() - saves changes without closing the screen. You can call this method from a custom event listener or override the default windowCommit action listener to perform some operations after the data has been saved, for example:

    @Override
    protected void commit(Action.ActionPerformedEvent event) {
        commitChanges().then(() -> {
            // this flag is used for returning correct outcome on subsequent screen closing
            commitActionPerformed = true;
            // perform actions after the data has been saved
        });
    }

    The default implementation of the commit() method shows a notification about successful commit. You can switch it off using the setShowSaveNotification(false) method on screen initialization.

  • validateAdditionalRules() method can be overridden for additional validation before saving changes. The method should store the information about validation errors in the 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 validateAdditionalRules(ValidationErrors errors) {
    if (getEditedEntity().getAddress().getCity() != null) {
        if (pattern.matcher(getEditedEntity().getAddress().getCity()).find()) {
            errors.add("City name cannot contain digits");
        }
    }
    super.validateAdditionalRules(errors);
}
Methods of MasterDetailScreen
  • getEditedEntity() - when the screen is in edit mode, returns an instance of the entity being edited. It’s the instance which is set in the data container of the form component. If the screen is not in edit mode, the method throws IllegalStateException.

  • validateAdditionalRules() method can be overridden for additional validation on saving changes as described above for StandardEditor.

3.5.1.1.3. Screen Events

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

See Decouple Business Logic with Application Events guide to learn how to use events on the UI layer.



InitEvent

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

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

InitEntityEvent is sent in screens inherited from StandardEditor and MasterDetailScreen before the new entity instance is set to edited entity container.

See also Initial Entity Values guide to learn how to use InitEntityEvent listeners.

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

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

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

BeforeCommitChangesEvent is sent in screens inherited from StandardEditor and MasterDetailScreen before saving of changed data by the commitChanges() method. In this event listener, you can check any conditions, interact with the user and abort or resume the save operation using the preventCommit() and resume() methods of the event object.

Let’s consider some use cases.

  1. Abort the save operation with a notification:

    @Subscribe
    public void onBeforeCommitChanges(BeforeCommitChangesEvent event) {
        if (getEditedEntity().getStatus() == null) {
            notifications.create().withCaption("Enter status!").show();
            event.preventCommit();
        }
    }
  2. Abort saving, show a dialog and resume after confirmation by the user:

    @Subscribe
    public void onBeforeCommitChanges(BeforeCommitChangesEvent event) {
        if (getEditedEntity().getStatus() == null) {
            dialogs.createOptionDialog()
                    .withCaption("Confirmation")
                    .withMessage("Status is empty. Do you really want to commit?")
                    .withActions(
                            new DialogAction(DialogAction.Type.OK).withHandler(e -> {
                                // resume with default behavior
                                event.resume();
                            }),
                            new DialogAction(DialogAction.Type.CANCEL)
                    )
                    .show();
            // abort
            event.preventCommit();
        }
    }
  3. Abort saving, show a dialog and retry commitChanges() after confirmation by the user:

    @Subscribe
    public void onBeforeCommitChanges(BeforeCommitChangesEvent event) {
        if (getEditedEntity().getStatus() == null) {
            dialogs.createOptionDialog()
                    .withCaption("Confirmation")
                    .withMessage("Status is empty. Do you want to use default?")
                    .withActions(
                            new DialogAction(DialogAction.Type.OK).withHandler(e -> {
                                getEditedEntity().setStatus(getDefaultStatus());
                                // retry commit and resume action
                                event.resume(commitChanges());
                            }),
                            new DialogAction(DialogAction.Type.CANCEL)
                    )
                    .show();
            // abort
            event.preventCommit();
        }
    }
AfterCommitChangesEvent

AfterCommitChangesEvent is sent in screens inherited from StandardEditor and MasterDetailScreen after saving of changed data by the commitChanges() method. Usage example:

@Subscribe
public void onAfterCommitChanges(AfterCommitChangesEvent event) {
    notifications.create()
            .withCaption("Saved!")
            .show();
}
BeforeCloseEvent

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

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

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

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

For example:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd"
        caption="Sample Screen"
        messagesPack="com.company.sample.web.screens.monitor">
    <layout>
    </layout>
</window>

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, by a standard action (when working with browse and edit entity screens), 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.

See Initial Entity Values guide to learn how to use external initialization via ScreenBuilders.

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. Note that in most cases you open such screens using standard actions (such as CreateAction or LookupAction), so you don’t have to use the ScreenBuilders API directly. However, the examples below can be useful if you don’t use standard actions but open a screen from a BaseAction or Button handler.

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.

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

If you are opening a screen using a standard action such as CreateAction, use its screenConfigurer handler to pass parameters via screen public setters.

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.

If you are opening a screen using a standard action such as CreateAction, use its screenOptionsSupplier handler to create and initialize the required ScreenOptions object.

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. This information can be obtained in two ways: by testing whether the screen was closed with one of standard outcomes defined by the StandardOutcome enum, or by getting the CloseAction object. The former approach is simpler while the latter is much more flexible.

Let’s consider the first approach: to close a screen with a standard outcome and test for it in the calling code.

The following screen is to be invoked:

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")
    public void onOkBtnClick(Button.ClickEvent event) {
        result = "Done";
        close(StandardOutcome.COMMIT); (1)
    }

    @Subscribe("cancelBtn")
    public void onCancelBtnClick(Button.ClickEvent event) {
        close(StandardOutcome.CLOSE); (2)
    }
}
1 - on "OK" button click, set some result state and close the screen with StandardOutcome.COMMIT enum value.
2 - on "Cancel" button click, close the with StandardOutcome.CLOSE.

In the AfterCloseEvent listener, you can check how the screen was closed using the closedWith() method of the event, 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.closedWith(StandardOutcome.COMMIT)) {
                        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:

package com.company.demo.web.screens;

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

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

    @Subscribe("okBtn")
    public void onOkBtnClick(Button.ClickEvent event) {
        close(new MyCloseAction("Done")); (1)
    }

    @Subscribe("cancelBtn")
    public void onCancelBtnClick(Button.ClickEvent event) {
        closeWithDefaultAction(); (2)
    }
}
1 - on "OK" button click, create the custom close action and set the result value in it.
2 - on "Cancel" button click, close with a default action provided by the framework.

In the AfterCloseEvent listener, you can get the CloseAction from the event and read the result value:

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

private void openOtherScreen() {
    Screen otherScreen = screens.create("demo_OtherScreen2", 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()); (3)
    }
}
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 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. By default, the framework provides the main screen with a side menu.

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 the main id. The main screen with side menu, by default, allows you to expand and collapse the side menu using the Collapse button located in the lower-left corner.

    The behavior of the side menu can be customized using SCSS variables (you can change these variables in the visual editor after creating a theme extension or a custom theme):

    • $cuba-sidemenu-layout-collapse-enabled enables or disables the side menu collapse mode. The default value is true.

    • $cuba-sidemenu-layout-collapsed-width specifies the width of the collapsed side menu.

    • $cuba-sidemenu-layout-expanded-width specifies the width of the expanded side menu.

    • $cuba-sidemenu-layout-collapse-animation-time specifies the time for the side menu to collapse and expand in seconds.

      When the $cuba-sidemenu-layout-collapse-enabled variable is set to false, the Collapse button is hidden, and the side menu is expanded.

  • 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.1.7. Screen Validation

The ScreenValidation bean can be used to run validation in screens. It has the following methods:

  • ValidationErrors validateUiComponents() is used by default when committing changes in the StandardEditor, InputDialog, and MasterDetailScreen. The method accepts a collection of components or a component container and returns validation errors in these components (ValidationErrors object). The validateUiComponents() method also can be used in an arbitrary screen. For example:

    @UiController("demo_DemoScreen")
    @UiDescriptor("demo-screen.xml")
    public class DemoScreen extends Screen {
        @Inject
        private ScreenValidation screenValidation;
        @Inject
        private Form demoForm;
    
        @Subscribe("validateBtn")
        public void onValidateBtnClick(Button.ClickEvent event) {
            ValidationErrors errors = screenValidation.validateUiComponents(demoForm);
            if (!errors.isEmpty()) {
                screenValidation.showValidationErrors(this, errors);
                return;
            }
        }
    }
  • showValidationErrors() - displays a notification with all errors and problematic components. The method accepts the screen and ValidationErrors object. It is also used by default in the StandardEditor, InputDialog, and MasterDetailScreen.

  • validateCrossFieldRules() - accepts a screen and an entity and returns the ValidationErrors object. Performs cross-field validation rules. Editor screens validate class-level constraints on the commit if the constraints include the UiCrossFieldChecks group and all attribute-level constraint checks are successful (see more information in the Custom Constraints section). You can disable this type of validation using the setCrossFieldValidate() method of the controller. By default, it is used in the StandardEditor, MasterDetailScreen, in the editor of the DataGrid. The validateCrossFieldRules() method also can be used in an arbitrary screen.

    As an example, let’s look at the Event entity for which we can define a class-level annotation @EventDate to check that the Start date must be lower than the End date.

    Event entity
    @Table(name = "DEMO_EVENT")
    @Entity(name = "demo_Event")
    @NamePattern("%s|name")
    @EventDate(groups = {Default.class, UiCrossFieldChecks.class})
    public class Event extends StandardEntity {
        private static final long serialVersionUID = 1477125422077150455L;
    
        @Column(name = "NAME")
        private String name;
    
        @Temporal(TemporalType.TIMESTAMP)
        @Column(name = "START_DATE")
        private Date startDate;
    
        @Temporal(TemporalType.TIMESTAMP)
        @Column(name = "END_DATE")
        private Date endDate;
    
        ...
    }

    The annotation definition looks like this:

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Constraint(validatedBy = EventDateValidator.class)
    public @interface EventDate {
    
        String message() default "The Start date must be earlier than the End date";
    
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default {};
    }
    EventDateValidator
    public class EventDateValidator implements ConstraintValidator<EventDate, Event> {
        @Override
        public boolean isValid(Event event, ConstraintValidatorContext context) {
            if (event == null) {
                return false;
            }
    
            if (event.getStartDate() == null || event.getEndDate() == null) {
                return false;
            }
    
            return event.getStartDate().before(event.getEndDate());
        }
    }

    Then you can use the validateCrossFieldRules() method in an arbitrary screen.

    @UiController("demo_DemoScreen")
    @UiDescriptor("demo-screen.xml")
    public class DemoScreen extends Screen {
    
        @Inject
        protected Metadata metadata;
        @Inject
        protected ScreenValidation screenValidation;
        @Inject
        protected TimeSource timeSource;
    
        @Subscribe("validateBtn")
        public void onValidateBtnClick(Button.ClickEvent event) {
            Event event = metadata.create(Event.class);
            event.setName("Demo event");
            event.setStartDate(timeSource.currentTimestamp());
    
            // We make the endDate earlier than the startDate
            event.setEndDate(DateUtils.addDays(event.getStartDate(), -1));
    
            ValidationErrors errors = screenValidation.validateCrossFieldRules(this, event);
            if (!errors.isEmpty()) {
                screenValidation.showValidationErrors(this, errors);
            }
        }
    }
  • showUnsavedChangesDialog() - shows the standard dialog for unsaved changes ("Do you want to discard unsaved changes?") with the Yes and No buttons. It is used in the StandardEditor. The showUnsavedChangesDialog() method has a handler that responds to user actions (the button that was clicked):

    screenValidation.showUnsavedChangesDialog(this, action)
                            .onDiscard(() -> result.resume(closeWithDiscard()))
                            .onCancel(result::fail);
  • showSaveConfirmationDialog() - shows the standard dialog for confirming saving changed data ("Do you want to save changes before close?") with the Save, Do not save, Cancel buttons. It is used in the StandardEditor. The showSaveConfirmationDialog() method has a handler that responds to user actions (the button that was clicked):

    screenValidation.showSaveConfirmationDialog(this, action)
                        .onCommit(() -> result.resume(closeWithCommit()))
                        .onDiscard(() -> result.resume(closeWithDiscard()))
                        .onCancel(result::fail);

You can adjust the dialog type using the cuba.gui.useSaveConfirmation application property.

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

Slider

gui_slider

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