- @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")
-
Preface
This manual provides the reference information for the CUBA platform and covers the most important topics of developing business applications with it.
Knowledge of the following technologies is required to use the platform:
-
Java Standard Edition
-
Relational databases (SQL, DDL)
Additionally, knowledge of the following technologies and frameworks will be helpful to get a deeper understanding of the platform:
This manual and other documentation related to the CUBA platform can be found at www.cuba-platform.com/manual. Video materials and presentations that can help you to understand the platform are available at www.cuba-platform.com/tutorials. You can also check out online demo applications at www.cuba-platform.com/online-demo.
If you have any suggestions for 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. Introduction
This chapter provides information about the CUBA platform features and requirements.
1.1. Overview
CUBA platform is an ideal tool for development teams working on line-of-business applications, typically having extensive data model, hundreds of screens and complex business logic.
Based on a mainstream technology stack, CUBA platform brings unparalleled productivity by utilizing a rich set of ready to use data-aware components, extensive scaffolding, visual interface designer and hot deploy.
Open architecture allows a developer to customize any part of the framework, providing high levels of control and flexibility. Developers have the freedom to use popular Java IDEs and have full access to the source code.
CUBA applications fit seamlessly into the corporate IT environment, supporting major databases and application servers, as well as popular aPaaS clouds. Streamlined clustered deployment ensures scalability and failover, while a generic REST API enables easy integration with other systems.
1.2. Technical Requirements
Minimum requirements for development using CUBA platform:
-
Memory – 4 GB
-
Hard drive space – 5 GB
-
Operating system: Microsoft Windows, Linux or macOS
1.3. Release Notes
CUBA platform changelog is available at http://files.cuba-platform.com/cuba/release-notes/6.9
2. Installation and Setup
Minimum software requirements are as follows:
- Java SE Development Kit (JDK) 8
-
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_152
.WarningJava 9 is not supported yet. You can build and run CUBA applications only on Java 8.
In order to build and run projects outside Studio, you need to set the path to the JDK root directory in the
JAVA_HOME
environment variable, e.g.C:\Program Files\Java\jdk1.8.0_152
.-
On Windows, you can do this at Computer → Properties → Advanced System Settings → Advanced → Environment variables. The value of the variable should be added to the System variables list.
-
On macOS, it is recommended to set
JAVA_HOME
in~/.bash_profile
:export JAVA_HOME=$(/usr/libexec/java_home -v 1.8)
If you install Java 9 on macOS, CUBA Studio and applications won’t start, regardless of the
JAVA_HOME
value. In this case, see the troubleshooting section below.
-
- Java IDE
-
IntelliJ IDEA or Eclipse. We recommend using IntelliJ IDEA (Community or Ultimate).
- 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 9+, Microsoft Edge.
- Troubleshooting
-
-
If you install Java 9 on macOS for some reason, CUBA Studio and applications won’t start. To recover from this situation, do the following:
-
Make you JDK 9 installation not used by default throughout the system: rename
/Library/Java/JavaVirtualMachines/jdk-9.0.1.jdk/Contents/Info.plist
file intoInfo.plist.disabled
(replacejdk-9.0.1.jdk
with the actual version of your JDK 9). -
Replace
JavaAppletPlugin.plugin
installed by Java 9 to the one from Java 8:-
Remove or rename
/Library/Internet Plug-Ins/JavaAppletPlugin.plugin
-
Install JDK 8 again, it will re-create the plugin.
-
-
Make sure you have specified
JAVA_HOME
with-v 1.8
argument as shown above. Re-login orsource .bash_profile
after making changes.
-
-
Make sure your environment does not contain
CATALINA_HOME
,CATALINA_BASE
andCLASSPATH
variables. They may cause problems starting Apache Tomcat web server which is used at development time. Reboot your computer after removing the variables.
-
2.1. CUBA Studio Installation
- Prerequisites
-
-
Make sure that Java SE Development Kit (JDK) 8 is installed by running the following command in the console:
java -version
The command should return the Java version, e.g.
1.8.0_152
. -
If you are using OpenJDK on Linux, install OpenJFX, for example:
sudo apt-get install openjfx
-
If you connect to the internet via a proxy server, some Java system properties must be passed to the JVM running Studio and Gradle. These properties are explained here: http://docs.oracle.com/javase/8/docs/technotes/guides/net/proxies.html (see properties for HTTP and HTTPS protocols).
It is recommended to set these properties system-wide in the
JAVA_OPTS
environment variable. The Studio launch script passesJAVA_OPTS
to the Java executable.
-
- Fresh installation of CUBA Studio
-
-
Download an appropriate installer or ZIP archive from https://www.cuba-platform.com/download.
-
Run the installer or unzip the archive to a local directory, e.g.
c:\work\studio
. -
Launch the installed application or open the command line, go to
bin
directory and runstudio.bat
orstudio
depending on your operating system. -
In the CUBA Studio Server window, enter the following parameters:
-
Server port − CUBA Studio server port (the default port is 8111).
-
Remote connection - by default, Studio accepts connections only from localhost. Select this checkbox if you need to connect to this Studio instance from a remote host.
-
Silent startup - if selected, the Studio server starts in tray and opens UI in default browser automatically. This option is available only for Windows.
-
-
Click Start to run the Studio server.
When the web server is started, the URL of the Studio interface will appear in the URL field. By clicking →, you can open the address in your default web browser; by clicking Copy you can copy the address to the clipboard.
-
Open the specified URL in the web browser and switch to the Settings tab in the Studio web interface. Enter the following parameters:
-
Java home − JDK installation to be used for building and running projects. If you have set the
JAVA_HOME
environment variable as described in the beginning of this chapter, it will appear in this field. Otherwise, Studio will try to find your Java installation itself. -
Gradle home - Gradle installation to be used for building projects. Leave it empty; in this case, the required Gradle distribution will be downloaded automatically.
If you want to use a local Gradle distribution, enter a path to the respective directory. Current version of the project build system is tested with Gradle 4.3.1.
-
IDE port − IDE plugin listening port (the default port is 48561).
-
Offline - enable working with projects without an Internet connection, provided that all the required libraries have been previously downloaded from the repository.
-
Check for updates - check for new versions on every start.
-
Send anonymous statistics and crash reports - enable Studio to send error statistics to developers.
-
Help language - built-in help language.
-
Logging level - the logging level: TRACE, DEBUG, INFO, WARN, ERROR, or FATAL. INFO by default.
-
-
Click Apply and proceed to projects.
-
Click Create new to create a new project, or Import to add an existing one to the Recent list.
-
Once the project is opened, the Studio will download the source code of the platform components and save it to the local folder. Before building the project, it is recommended to wait until the download is finished and make sure that the background task indicator in the bottom left corner has faded out.
-
- Updating CUBA Studio
-
If you are updating Studio to a newer bug-fix version (e.g. from 6.5.0 to 6.5.1), install it to the existing folder, e.g. on Windows it would be
C:\Program Files (x86)\CUBA Studio 6.5
. When installing a new minor or major version, use a separate folder, e.g.CUBA Studio 6.6
.If installed from Windows EXE installer or ZIP archive, Studio supports auto-update on newer bug-fix releases. Update files are saved in the
~/.haulmont/studio/update
folder. In case of any problems with the new version, you can remove the update files and Studio will revert to the version installed manually.Auto-update does not work for minor and major releases and if Studio was installed from macOS DMG. In this case, you should download new installer and run it manually.
2.2. IDE Integration
Take the following steps to integrate Studio with IntelliJ IDEA or Eclipse:
-
Open or create a new project in the Studio.
-
Switch to Project properties section and click Edit. Select the required Java IDE by checking IntelliJ IDEA or Eclipse.
-
Select Build > Create or update <IDE> project files in the Studio menu. The corresponding files will be created in the project directory.
-
For IntelliJ IDEA integration:
-
Run IntelliJ IDEA 13+ and install CUBA Framework Integration plugin, from the plugin repository: File > Settings > Plugins > Browse Repositories.
-
-
For Eclipse integration:
-
Run Eclipse 4.3+, open Help > Install New Software, add
http://files.cuba-platform.com/eclipse-update-site
repository and install the CUBA Plugin. -
In the CUBA section of the Window > Preferences menu, check Studio Integration Enabled, and click OK.
-
Please note that IDE: on port 48561 label has appeared in the bottom left corner of the Studio. Now the corresponding source code files will be opened in IDE when you click IDE buttons in the Studio.
3. Quick Start
This section describes the process of creating an application using CUBA Studio. Similar information is provided in the videos available at www.cuba-platform.com/quickstart.
Make sure that the necessary software is already installed and set up on your computer, see Installation and Setup.
Key stages of our application development:
-
Data model development including creation of entities describing application domain and corresponding database tables.
-
Development of the user interface screens enabling to create, view, update and delete data model entities.
3.1. Application Details
The application should maintain information about the customers and their orders.
A customer has the following attributes:
-
Name
-
Email
Order attributes:
-
Reference to a customer
-
Date
-
Amount
The application UI should contain:
-
Customers browser screen;
-
Customer editor screen, containing as well the list of this customer’s orders;
-
General orders browser screen;
-
Order editor screen.
3.2. Creating a Project
-
Start CUBA Studio and open its web interface (See CUBA Studio Installation).
-
Click Create new.
-
Specify the name of the new project in the Project name field of the New project window – for example,
sales
. The name should contain only Latin letters, numbers and underscores. Think carefully on the project name at this stage, as changing it later on will require complex manual intervention. -
The following fields below will be automatically populated:
-
Project path – the path to the new project directory. You can select the directory manually by clicking the … button next to the field. The Select folder window will appear with the list of folders n your hard drive. You can select one of those, or create a new directory by clicking the + button.
-
Project namespace – the namespace which will be used as a prefix for entity names and database tables. The namespace can consist of Latin letters only and should be as short as possible. For example, if the project name is
sales_2
, the namespace can besales
orsal
. -
Root package − the root package of Java classes. It can be adjusted later, but the classes generated at project creation will not be moved.
-
Repository − binary artifacts repository URL and authentication parameters.
-
Platform version – the platform version used in the project. The platform artifacts will be automatically downloaded from the repository on project build.
-
-
Click OK. Empty project will be created in the specified
sales
directory and the main Studio window will open. -
Assemble the project: select option Build > Assemble project in the Studio main menu. At this stage all required libraries will be downloaded and project artifacts will be assembled in
build
subdirectories of the modules. -
Create the database on the local HyperSQL server: select option Run > Create database in the menu. The database name is the same as project namespace by default.
-
Select Run > Deploy menu option. Tomcat server with the deployed application will be installed in the project
deploy
subdirectory. -
Select Run > Start application server option. The link next to the Web application caption on the status panel will become available in a few seconds so you will be able to open the application directly from Studio.
The username and password are
admin
/admin
.The running application contains two main menu items (Administration and Help), as well as security and administration subsystems functionality.
3.3. Creating Entities
Let’s create the Customer
entity class.
-
Go to the Data Model tab in the navigation section and click New > Entity. The New entity dialog window will appear.
-
Enter the name of the entity class –
Customer
– in the Class name field. -
Click OK. The entity designer page will be displayed in the workspace.
-
The entity name and the database table name will be automatically generated in the Name and the Table fields respectively.
-
Leave the existing value –
StandardEntity
- in the Parent class field. -
Leave the Inheritance strategy field blank.
Next, let’s create entity attributes. To do this, click the New button below the Attributes table.
-
Create attribute window will appear. Enter the name of the entity attribute −
name
, in the Name field. SelectDATATYPE
value in the Attribute type list, specifyString
attribute type in the Type field and then set the length of the text attribute to 100 characters in the Length field. Check the Mandatory box. The name of the database table column will be automatically generated in the Column field.Click Add to add the attribute.
-
email
attribute is created in the same way but the value in Length field should be set to50
.
After creating the attributes, go to the Instance Name tab in the entity designer to specify the Name pattern. Select the name
attribute in the Available attributes list and move it to the Name pattern attributes list by clicking the button with the right arrow on it.
Customer entity creation is now complete. Click OK on the top panel to save the changes and close the page.
Let’s create the Order
entity.
Click New > Entity on the Data Model tab. Enter the Class name − Order
. The entity should have the following attributes:
-
Name −
customer
, Attribute type −ASSOCIATION
, Type −Customer
, Cardinality −MANY_TO_ONE
. -
Name −
date
, Attribute type −DATATYPE
, Type −Date
. Check Mandatory box fordate
attribute. -
Name −
amount
, Attribute type −DATATYPE
, Type −BigDecimal
.
3.4. Creating Database Tables
It is sufficient to click Generate DB scripts button in Data Model tab on the navigation panel to create database tables. After that, Database Scripts page will open. Both incremental DB update scripts from the current state (UPDATE SCRIPTS) and initial DB creation scripts (INIT TABLES, INIT TABLES, INIT DATA) will be generated on this page.
Click Save and close button to save the generated scripts. To run update scripts, stop the running application using the Run > Stop application server command, then select Run > Update database.
3.5. Creating User Interface Screens
Now we will create screens for customers and orders data management.
3.5.1. Screens for Customer
Select Customer
entity in the Data Model tab on the navigation panel to create standard screens for viewing and editing Customers. Click New > Generic UI screen at the top of the section. After that, the template browser page will appear.
Select Entity browser and editor screens in the list of available templates.
All fields in this dialog are already populated with default values, there is no need to change them. Click Create and then Close buttons.
customer-browse.xml
and customer-edit.xml
items will appear in Web Module on Generic UI tab of the navigation panel.
3.5.2. Order Screens
Order
entity has the following distinction: since one of the attributes is the Order.customer
reference attribute, you should define a view including this attribute (standard _local
view does not include reference attributes).
Go to the Data Model tab on the navigation panel, select the Order
entity and click the New > View button. View designer page will open. Enter order-with-customer
as the view name, click on customer
attribute and select _minimal
view for the Customer
entity on the panel on the right.
Click OK on the top panel.
After that, select the Order
entity and click New > Generic UI screen.
Select order-with-customer
in the View fields for both browser and editor templates and click Create.
order-edit.xml
and order-browse.xml
items will appear in the Web Module on the Generic UI tab of the navigation panel.
3.5.3. Application Menu
At the moment of their creation, the screens were added to the application menu item of the default application menu. Let’s rename it. Switch to the Generic UI tab on the navigation panel and click Open web menu. The Menu Designer page will open. Select the application-sales
menu item to edit its properties.
Enter the new value of the menu identifier − shop
− in the Id field, then click OK on the top panel.
3.5.4. Customer Editor With a List of Orders
Do the following to display the list of Orders in the Customer’s edit screen:
-
Go to the Generic UI tab on the navigation panel. Choose
customer-edit.xml
screen and click Edit. -
Go to the Datasources tab on the screen designer page and click New.
-
Select the newly created datasource in the list. Its attributes will appear in the right part of the page.
-
Specify
collectionDatasource
in the Type field. -
Select
Order
entity in the Entity list. -
The data source identifier −
ordersDs
- will be automatically generated in Id field. -
Select
_local
view in the View list. -
Add the WHERE clause to the query generated in the Query field:
select e from sales$Order e where e.customer.id = :ds$customerDs order by e.date
The query contains orders selection criterion with
ds$customerDs
parameter. The parameter value named likeds${datasource_name}
will contain id of the entity selected indatasource_name
datasource at the moment, in this case it is the id of the Customer being edited. -
Click Apply to save the changes.
-
Next go to the Layout tab in the screen designer and find the
Label
component in the components palette. Drag this component to the screen components hierarchy panel and place it betweenfieldGroup
andwindowActions
. Go to the Properties tab on the properties panel. Enter the label valueOrders
in the value field.TipIf the application is intended to be used in multiple languages, use the button next to the value field to create the new message
msg://orders
and define label values in required languages. -
Drag
Table
from the components palette to components hierarchy panel and place it betweenlabel
andwindowActions
. Select this component in the hierarchy and specify table size in the Properties tab: set100%
in the width field and200px
in the height field. ChooseorderDs
from the list of available datasources. Then generate the table identifier using the button next to the id field:ordersTable
. -
Click OK on the top panel to save the changes in the screen.
3.6. Running the Application
Now let’s see how the created screens look in the actual application. Select Run > Start application server.
Log in using default credentials in the login window. Open the Shop > Customers menu item:
Click Create and create a new customer:
Open the Shop > Orders menu item:
Click Create and create a new order, selecting the newly created customer in the Customer field:
The new order is now displayed in the customer’s editor:
4. Cookbook
This collection of practical recipes for developing on CUBA platform contains examples of implementing typical use cases and solving common problems. The information in each section is organized from basic to advanced topics, so feel free to jump to another section or leave the documentation at any time and start coding.
Most of the sections are accompanied by the sample applications. You can see them online, view their source code on GitHub or download and run locally. You will also see the applications on the Samples tab in Studio.
Tip
|
The cookbook is a work in progress and will be gradually improved. If you have any ideas for new recipes which are worth demonstrating, feel free to create issues in the source repository on GitHub. |
4.1. Organizing Business Logic
When you start developing on the platform, one of the first questions is "where should I put my business logic"? Using Studio for creating data model and CRUD screens is simple, but any real project requires some logic beyond CRUD. This section explains how you can effectively organize your business logic depending on your requirements.
Most examples in this section work with the following data model:
In these examples, we will calculate discounts for customers based on total amount of their purchases.
4.1.1. Business Logic in Controllers
If we want to run the discount calculation when a user clicks a button on the customer’s browser screen, the most straightforward way to accomplish this is to put the calculation logic right in the browser screen controller.
See the Calculate discount button in the demo application and the screen controller implementation: CustomerBrowse.java. Please keep in mind that the provided calculation process is not optimal and see more options in the Loading and Saving Data section.
This approach is acceptable if the logic is invoked from a single point and it is not too complex to fit into a couple of short methods.
4.1.2. Using Client Tier Beans
Let’s complicate the task from the previous section a bit. Now we want to invoke the calculation both from the customer browser and editor screens. To not repeat yourself, we should extract the logic to a common place available for both controllers. It can be a managed bean of the client tier.
A managed bean is a class annotated with the @Component
annotation. It can be injected into other beans and screen controllers, or obtained via the AppBeans.get()
static method. If the bean has a separate interface, you can access the bean through the interface instead of the class.
Please note that in order to be accessible for screen controllers, the bean must be located in global, gui or web modules of your project. In the former case the bean will be also accessible for the middleware.
See the Calculate discount button on both browser and editor screens of the demo application and the implementation:
-
CustomerBrowse.java - browser controller.
-
CustomerEdit.java - editor controller.
-
DiscountCalculator.java - discount calculator bean. It uses DataManager to load the list of orders for the given customer from the database.
4.1.3. Using Middleware Services
In the previous section we considered the encapsulation of business logic in a managed bean of the client tier. Now we will go further and implement our logic in the most appropriate place: on the middle tier. By doing this, we will achieve the following goals:
-
Our business methods will be available for all types of clients including Polymer UI.
-
We will be able to use APIs available only on the middleware: EntityManager, transactions, etc.
In order to invoke a middleware business method from the client, you need to create a service. Studio can help you to scaffold the service stub:
-
Switch to the Middleware section and click New > Service.
-
Change the service interface name to
DiscountService
. The bean class and service names will be changed accordingly. Click OK or Apply. -
Click IDE and open the service interface in your IDE. Create a method and implement it in the service class.
See an example implementation in the demo application:
-
CustomerBrowse.java and CustomerEdit.java - screen controllers that invoke the service.
-
DiscountService.java - service interface.
-
DiscountServiceBean.java - service implementation.
-
DiscountCalculator.java - a managed bean of the middle tier which actually calculates discounts. Of course, a service can contain the business logic itself, but we will use this delegate to share logic with entity listeners and JMX beans (see next sections).
Please note that this bean is different from the one mentioned in the previous section: it is located in the core module and uses EntityManager for loading the amount of purchases from the database.
Let’s now make our business method accessible for external clients through the REST API:
-
Open the service editor in Studio and switch to the REST Methods tab.
-
Select the REST invocation allowed checkbox for the method.
Studio will create the rest-services.xml
file and write the method description into it. After restarting the application server you will be able to invoke your business method using HTTP requests. For example, the following GET request should work with our online demo server:
https://demo1.cuba-platform.com/business-logic/rest/v2/services/sample_DiscountService/calculateDiscount?customerId=1797f54d-5bec-87a6-4330-d958955743a2
Please note that the demo application allows anonymous access. In the most real-world usage scenarios you need to authenticate prior to executing REST requests.
4.1.4. Using Entity Listeners
Entity listeners allow you to execute your business logic each time an entity is added, updated or removed from the database. For example, we could recalculate the discount for a customer each time an order for this customer is changed.
An entity listener stub can be easily created using Studio:
-
Switch to the Middleware section and click New > Entity listener.
-
Change the class name to
OrderEntityListener
and select checkboxes forBeforeInsertEntityListener
,BeforeUpdateEntityListener
andBeforeDeleteEntityListener
interfaces. -
Select
Order
entity in the Entity type field. -
Click OK or Apply and open the listener class in your IDE.
See an example implementation in the demo application:
-
OrderEntityListener.java - the entity listener.
-
DiscountCalculator.java - a managed bean of the middle tier which actually calculates discounts. An entity listener can contain the business logic itself, but we will use this delegate to share logic with services and JMX beans.
If you open the Logic in Entity Listeners screen of the demo application, you will see two tables: orders and customers. Create, edit or remove an order, then refresh the customers table, and you will see that the discount of the corresponding customer is changed.
4.1.5. Using JMX Beans
With JMX beans you can expose some administrative functionality of your application without creating a user interface for it. The functionality becomes available via the built-in JMX console and via external JMX tools like jconsole
.
In our example with discounts, a user having access to JMX console is able to recalculate discounts for all customers and for a customer with a given id.
Studio cannot help you with scaffolding JMX beans at the moment, so all classes and configuration entries have to be created manually in the IDE.
See an example implementation in the demo application:
-
DiscountsMBean.java - JMX bean interface.
-
Discounts.java - JMX bean implementation.
-
DiscountCalculator.java - a managed bean of the middle tier which is invoked by the JMX bean. A JMX bean can contain the business logic itself, but we will use this delegate to share logic with services and entity listeners.
-
spring.xml - registers the JMX bean.
4.1.6. Running Code on Startup
Sometimes you need to run some code on the application startup, at the moment when all application functionality is already initialized and ready to work. For this, you can use application lifecycle event listeners.
In this section we demonstrate how to dynamically register an entity listener on application startup. Consider the following task: a project has an Employee
entity that is linked one-to-one to the platform’s User
entity.
If the name
attribute of the User
entity is changed, for example, through a standard user management screen, the name
attribute of the related Employee
should change as well. This is a common task for "denormalized" data, which is typically solved using entity listeners. Our case is more complicated, since we need to track changes of the platform’s User
entity, and thus we cannot add an entity listener using the @Listeners annotation. So we will add a listener dynamically using the EntityListenerManager
bean on application start.
-
AppLifecycle.java - a middleware bean listening to the
AppContextInitializedEvent
events. -
UserEntityListener.java - an entity listener for the
User
entity.
As a result, the initEntityListeners()
method of the AppLifecycle
class will be invoked on the middleware block startup. This method registers the sample_UserEntityListener
bean as an entity listener for the User
entity.
The onBeforeUpdate()
method of the UserEntityListener
class will be invoked every time before the changes in the User
instances are saved to the database. The method checks if the name
attribute exists among the updated attributes. If yes, a related Employee
instance is loaded and its name
is updated with the new value.
4.2. Modeling Problem Domain
In this section, you can find recipes for the data model design and working with entity attributes.
4.2.1. Assigning Initial Values
There are different ways to assign initial values to the attributes of new entity instances.
4.2.1.1. Entity Fields Initialization
Simple attributes (Boolean
, Integer
etc.) and enumerations can be initialized in the declaration of the corresponding field of an entity class, see for example active
and grade
fields in Customer.java.
Additionally, a specific initialization method with a @PostConstruct annotation can be created in the entity class. In this case, any global infrastructure interfaces and beans can be invoked during initialization, see for example the init()
method in Customer.java.
4.2.1.2. Initialization Using CreateAction
If the initial value of an attribute depends on the data of the invoking screen, you can use setInitialValues()
or setInitialValuesSupplier()
methods of the CreateAction class.
See an example of handling Customer
and CustomerAddress
entities in the demo application:
-
customer-address-browse.xml - a screen descriptor with two linked tables, one for customers and another for their addresses.
-
CustomerAddressBrowse.java - the screen controller. In its
init()
method, thesetInitialValuesSupplier()
is used to provide initial value forcustomer
attribute of a created address. It will be the currently selected in the first table customer.
4.2.1.3. Using initNewItem Method
Initial values can also be defined in the initNewItem() method of the screen controller of the created entity.
Consider the following entities:
In the demo application, CustomerDetails
attribute (info
) is edited on the same screen as Customer
itself. It requires creating of a CustomerDetails
instance together with the owning Customer
.
-
customer-edit.xml - a customer edit screen descriptor. It contains a nested datasource for a linked
CustomerDetails
instance. TheinfoField
text area component is connected to this datasource. -
CustomerEdit.java - the screen controller. It defines the
initNewItem()
method that creates a newCustomerDetails
instance and sets it to a newCustomer
. The created instance will be available through the nested datasource and later saved to the database when the screen is committed.
4.2.2. Composite Structures
CUBA platform supports two types of relationship between entities: association and composition. They are called ASSOCIATION and COMPOSITION respectively in the CUBA Studio interface. Association is a relationship between the objects that can exist separately from each other. Composition, on the other hand, is used for "master-detail" relations, when the detail instances can exist only as part of the master. A case of an airport and its terminals may be considered an example of composition: a terminal that does not belong to any airport does not make sense.
Typically, the entities belonging to a composition are edited together since it is more natural. For example, a user opens the airport editing screen and sees the list of terminals, so the user can create and edit them, but all changes both for the airport and the terminals are saved to the database together in one transaction, and only after the user confirms saving of the master entity (the airport).
4.2.2.1. One-to-Many: One Level of Nesting
Let’s implement a one-to-many composition using the Airport
and the Terminal
entities as an example:
-
Terminal.java - the
Terminal
entity contains a mandatory link to theAirport
.In the Studio entity designer, set for the
airport
attribute: Attribute type - ASSOCIATION, Cardinality - MANY_TO_ONE, Mandatory - on. -
Airport.java - the
Airport
entity contains a one-to-many collection of terminals. The corresponding field is annotated with @Composition in order to implement composition, and @OnDelete for cascaded soft delete.In the Studio entity designer, set for the
terminals
attribute: Attribute type - COMPOSITION, Cardinality - ONE_TO_MANY, On delete - CASCADE. -
views.xml - the
airport-terminals
view of the airport editing screen contains theterminals
collection attribute. We are using the_local
view for this attribute, because theairport
attribute of theTerminal
entity is set only at the creation of a newTerminal
instance and never changes after that, so we do not need to load it. -
airport-edit.xml - the XML descriptor of the airport editor defines a datasource for the
Airport
instance and a nested one for its terminals. It also contains a table displaying terminals. -
terminal-edit.xml - a standard editor for the
Terminal
entity.
As a result, editing of an airport instance works as follows:
-
The airport edit screen shows a list of terminals.
-
A user can pick a terminal and open its editor. When OK is clicked in the terminal editor, the updated instance of the terminal is not saved to the database, but to the
terminalsDs
datasource of the airport editor. -
The user can create new terminals and delete existing ones. All changes will be saved to the
terminalsDs
datasource. -
When a user clicks OK in the airport edit screen, the updated
Airport
instance together with all the updatedTerminal
instances is submitted to the DataManager.commit() method on the middleware and saved to the database within a single transaction.
4.2.2.2. One-to-Many: Two Levels of Nesting
Composition can be deeper, with up to two nested levels. Let’s extend the previous example by adding a MeetingPoint
entity describing a meeting point at an airport terminal:
The Terminal
entity contains the meetingPoints
attribute – a collection of the MeetingPoint
instances. In order for all three entities to become a single composition and be edited together, the following should be done in addition to the steps described above:
-
Terminal.java - the
meetingPoints
attribute of theTerminal
class is marked as@Composition
and@OnDelete
similarly to theterminals
attribute of theAirport
class. -
views.xml - the
terminal-meetingPoints-view
view of theTerminal
class contains themeetingPoints
collection attribute. This view is used in theairport-terminals-meetingPoints-view
view of theAirport
entity. -
airport-edit.xml - the
Airport
edit screen XML descriptor contains datasources for an instance of theAirport
and nested entities for the entire composition (airportDs
>terminalsDs
>meetingPointsDs
).WarningHere, the
meetingPointsDs
datasource is not associated with any visual components, however it is needed for correct operation of joint editing of the composition. -
terminal-edit.xml - the terminal edit screen XML descriptor contains a nested datasource and a corresponding table for the
meetingPoints
collection.
As a result, the updated instances of the MeetingPoint
, as well as the Terminal
instances, will be saved to the database only with the Airport
instance in the same transaction.
4.2.2.3. One-to-Many: Three Levels of Nesting
Suppose that you need an additional entity that contains some details of the meeting point: Note. So the whole structure looks as follows: Airport > Terminal > Meeting Point > Note.
CUBA can handle compositions with up to 2 levels of nesting. Here we have 3 levels, so we should limit the depth either from the top or from the bottom. Below we consider two different approaches (from the user experience perspective) of excluding the airport from the composition. Both of them solve the same problem: as now terminals are saved to the database independently from the airport, you cannot save a terminal for a newly created airport which is not saved to the database yet.
-
In the first approach, the airport browser and editor look the same as above, but the editor has additional Save button to save a new airport without closing the screen. A user cannot create terminals until the new airport is saved.
-
airport-edit.xml contains a standalone datasource for terminals instead of the nested one. This standalone datasource is linked to the airport datasource and thus loads terminals for the edited airport. Besides, airport editor contains
extendedEditWindowActions
frame which allows a user to save airport without closing the screen. -
AirportEdit.java - here in the
postInit()
method of the airport editor, we manage the enabled state of the terminal’s Create action and pass the current airport instance to initialize the airport attribute of a created terminal.
-
-
In the second approach, we have split the airport browser into two panels: one for the list of airports and another for the dependent list of terminals. That is the list of terminals is now outside of the airport editor. The terminal’s Create action is disabled until an airport is selected.
-
airport-browse.xml contains a standalone datasource for the list of terminals. It is linked to the airports datasource and thus loads terminals for a selected airport.
-
AirportBrowse.java - here in the
init()
method of the airport browse controller, we manage the enabled state of the terminal’s Create action and pass the currently selected airport instance to initialize the airport attribute of a created terminal.
-
4.2.2.4. One-to-One Composition
The one-to-one composition will be illustrated by the Customer
and CustomerDetails
entities:
-
Customer.java - the
Customer
entity contains an optional link toCustomerDetails
annotated with@Composition
. -
CustomerDetails.java - the
CustomerDetails
entity. -
customer-edit.xml - the customer edit screen descriptor. It contains a nested datasource for the
CustomerDetails
instance. In order to load the nested instance, the root datasource uses a view of theCustomer
entity that includes thedetails
attribute. The field group in the customer edit screen just declares a field for thedetails
attribute.
As a result, customer editing works as follows:
-
The customer edit screen contains the PickerField component with two actions: OpenAction and ClearAction:
-
When the open action is invoked, a new instance of
CustomerDetails
is created and its edit screen is shown. When OK is clicked in the details editor, the details instance is not saved to the database, but to thedetailsDs
datasource of the customer edit screen. -
The picker field displays the instance name of the details entity:
-
When a user clicks OK in the customer edit screen, the updated
Customer
instance together with theCustomerDetails
instance is submitted to theDataManager.commit()
method on the Middleware and saved to the database within a single transaction. -
If the user invokes the clear action of the picker field, the
CustomerDetails
instance is deleted and the reference to it is cleared in the same transaction after the user commits the customer editor.
4.2.3. Many-to-Many Associations
The many-to-many association implies that multiple records in one table are related to multiple records in another table. The joining table will store primary keys of both related entities. Optionally, this table may contain additional columns.
Depending on whether you need additional fields in the joining table, you can implement many-to-many relationship with an additional entity or without it. The following examples illustrate both approaches.
4.2.3.1. Direct Many-to-Many Association
Let’s implement a many-to-many association using the Airport
and the Airline
entities as an example. An airport can handle many airlines, and an airline carrier, in turn, can provide service to many airports:
-
Airport.java - the
Airport
entity contains a many-to-many list of airlines.In the Studio entity designer, set for the
airlines
attribute: Attribute type -ASSOCIATION
, Cardinality -MANY_TO_MANY
.Airport
will be marked as the owning side of the relationship, and Studio will ask you to create the correspondingairports
attribute in theAirline
entity as the inverse side of the relationship.@JoinTable(name = "SAMPLE_AIRLINE_AIRPORT_LINK", joinColumns = @JoinColumn(name = "AIRPORT_ID"), inverseJoinColumns = @JoinColumn(name = "AIRLINE_ID")) @ManyToMany protected List<Airline> airlines;
-
Airline.java - the
Airline
entity now contains the many-to-many list of airports: Attribute type -ASSOCIATION
, Cardinality -MANY_TO_MANY
.@JoinTable(name = "SAMPLE_AIRLINE_AIRPORT_LINK", joinColumns = @JoinColumn(name = "AIRLINE_ID"), inverseJoinColumns = @JoinColumn(name = "AIRPORT_ID")) @ManyToMany protected List<Airport> airports;
Airline
will be also marked by default as the owning side of the relationship, which enables modification of the collections on the both sides. -
views.xml - the
airport-airlines
view of the airport editing screen contains the airlines association attribute with the_minimal
view. Theairline-airports
view includes the airports association as well. -
airport-edit.xml - the XML descriptor of the airport editor defines a datasource for the
Airport
instance and a nested one for its airlines. It also contains a table displaying airlines and the actions add and remove. -
airline-edit.xml - the XML descriptor of the airline editor defines a datasource for the
Airline
instance and a nested one for its airports. It also contains a table displaying airports and the actions add and remove.So, the
Airport
and theAirline
editors are absolutely symmetrical.
As a result, editing of an airline instance works as follows:
The airline edit screen shows a list of airports.
A user can click Add, the Airport
lookup will be opened, and the user can either select an airport to add or open its editor. When OK is clicked in the airport editor, the updated instance of the airport is saved both to the database and to the airportsDs
datasource of the airline editor, as the Airport
entity is fully independent.
The user can create new airports and delete existing ones, and all changes will be saved to the database in a separate transactions and to the airportsDs
datasource.
When a user clicks OK in the airline edit screen, the updated Airline
instance together with newly created links to the Airport
instances is submitted to the DataManager.commit() method on the middleware and saved to the database.
It works absolutely the same way from the other side in the Aiport
editor.
4.2.3.2. Many-to-Many Association with Link Entity
The many-to-many association is always implemented using a joining table, but creating an entity to reflect this table is optional. The joining entity can be created in case you want to store some additional fields in the joining table.
Let’s demonstrate this approach using the Airport
and the DutyFree
entities as an example. Many different duty-free shops can be located in one airport, and one duty-free shop can be represented in many different airports. Suppose that we want to store the link airport-shop and the currency used in this shop and this airport:
-
Airport.java - the
Airport
entity contains a one-to-many composition ofAirportDutyFree
instances.In the Studio entity designer, set for the
dutyFreeShops
attribute: Attribute type -COMPOSITION
, Cardinality -ONE_TO_MANY
.@Composition @OnDelete(DeletePolicy.CASCADE) @OneToMany(mappedBy = "airport") protected List<AirportDutyFree> dutyFreeShops;
-
DutyFree.java - the
DutyFree
entity contains a one-to-many composition ofAirportDutyFree
instances, too.In the Studio entity designer, set for the
airports
attribute: Attribute type -COMPOSITION
, Cardinality -ONE_TO_MANY
.@Composition @OnDelete(DeletePolicy.CASCADE) @OneToMany(mappedBy = "dutyFree") protected List<AirportDutyFree> airports;
-
AirportDutyFree.java - thus, the
AirportDutyFree
entity contains two many-to-one references:airport
anddutyFree
.@ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "AIRPORT_ID") protected Airport airport; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "DUTY_FREE_ID") protected DutyFree dutyFree; @Column(name = "CURRENCY") protected Integer currency;
-
views.xml - the
airport-duty-free
view of the airport editing screen contains the composition ofdutyFreeShops
(referencing theAirportDutyFree
joining entity) withdutyFree
andcurrency
attributes.The
dutyFree-airport
view follows the same logic: it includes the composition ofairports
(referencing theAirportDutyFree
joining entity) withairport
andcurrency
attributes. -
duty-free-edit.xml - the XML descriptor of the duty-free shop editor defines a datasource for the
DutyFree
instance and a nested one for its airports. It also contains a table displaying airports and the custom action to pick an airport directly, bypassing theAirportDutyFree
editor.
As a result, editing of a DutyFree
instance works as follows:
The DutyFree
edit screen shows a list of airports and the currency drop-down.
A user can click Add airport, the Airport
lookup will be opened, and the user can either select an airport to add or open its editor. When the user selects an airport, the new AirportDutyFree
instance is created with the default currency. This instance it is not saved to the database, but added to the airportsDs
datasource of the DutyFree
editor.
When OK is clicked in the Airport
editor, the updated instance of the airport is saved both to the database and to the airportsDs
datasource of the DutyFree
editor, as the Airport
entity is fully independent.
The user can create new airports and delete existing ones, and all changes will be saved to the database in separate transactions and to the airportsDs
datasource as well.
When a user clicks OK in the duty-free edit screen, the updated DutyFree
instance together with all the updated AirportDutyFree
instances is submitted to the DataManager.commit() method on the middleware and saved to the database within a single transaction.
4.2.4. Entity Inheritance
This section demonstrates how to use entity inheritance in CUBA applications.
Let’s assume that we have two types of clients - companies and individuals, which have some common attributes. We would like to store the common attributes in one table and use separate linked tables to store specific attributes.
So, the data model contains the Client
entity, which is a base class stored in the SAMPLE_CLIENT
table. The Company
and Person
entities are stored in separate tables with foreign keys that reference the base table.
The Order
entity has a reference to Client
. As the client can be of the different types, when users create orders, they should be able to choose the type of the client.
Client.java entity:
-
Inheritance strategy is set to
JOINED
-
Discriminator column name
DTYPE
and type String are left default -
Discriminator value is set to
C
Company.java entity:
-
Parent class is set to
Client
-
Discriminator value is set to
M
Person.java entity:
-
Parent class is set to
Client
-
Discriminator value is set to
P
The OrderEdit.java screen controller contain the Client selection components and logic.
4.3. Working with Generic UI
This section of the cookbook covers topics related to Generic UI, which is a primary frontend development technology in CUBA applications.
4.3.1. Screen Layout Rules
Below we explain how to properly place visual components and containers on your screens.
4.3.1.1. Positioning of Components
- Size types
- Content-dependent size
-
The component will take enough space to fit its content.
Examples:
-
For Label, the size is defined by text length.
-
For containers, the size is defined by the sum of all component sizes inside a container.
XML<label width=”AUTO”/>
Javalabel.setWidth(Component.AUTO_SIZE);
Components with content-dependent size will adjust their dimensions during screen layout initialization or when the content size is changed.
-
- Fixed size
-
Fixed size implies that the component dimensions will not change at runtime.
XML<vbox width=”320px” height=”240px”/>
Javavbox.setWidth(”320px”);
- Relative size
-
Relative size indicates the percentage of available space that will be occupied by the component.
XML<label width=”100%”/>
Javalabel.setWidth(”50%”);
Components with relative size will react to changes in the amount of the available space and adjust their actual size on the screen.
- Container specifics
-
By default, containers without the expand attribute provide equal space for all nested components. Exceptions: flowBox and htmlBox.
For example:
<layout> <button caption="Button"/> <button caption="Button"/> </layout>
Components and containers width and height are content-dependent by default. Some containers have different default dimensions:
Container Width Height 100%
AUTO
100%
AUTO
100%
AUTO
The root layout element is a vertical container (
VBox
), which has 100% width and height. The height can beAUTO
in dialog mode.Tabs within a TabSheet are VBox containers.
GroupBox
component contains aVBox
or anHBox
, depending on the orientation property value.Example of a container with content-based size:
<layout> <vbox> <button caption="Button"/> <button caption="Button"/> </vbox> </layout>
Example of a container with relative size:
<layout spacing="true"> <groupBox caption="GroupBox" height="100%"/> <button caption="Button"/> </layout>
Here,
layout
, as well asvbox
orhbox
, provides equal space to all nested components, andgroupBox
has 100% height. In addition to that,groupBox
has 100% width by default and takes all the available space.
- Component specifics
-
It is recommended to set the absolute or relative height for Table and Tree. Otherwise, a table/tree can take unlimited size, if there are too many rows or nodes.
ScrollBox must have fixed or relative (but not AUTO) width and height. Components inside
ScrollBox
, positioned in the scrolling direction, may not have relative dimensions.The following examples show the correct use of horizontal and vertical
ScrollBox
containers. If scrolling is required in both directions, bothheight
andwidth
must be set for the components (AUTO or absolute).
- The expand option
-
The container’s expand attribute allows specifying the component that will be given maximum available space.
The component specified in
expand
will have 100% size in the direction of the component expansion (vertically - forVBox
, horizontally - forHBox
). When container size is changed, the component will change its size accordingly.<vbox expand="bigBox"> <vbox id="bigBox"/> <label value="Label"/> </vbox>
expand
works relatively to component expansion, for example:<layout spacing="true" expand="groupBox"> <groupBox id="groupBox" caption="GroupBox" width="200px"/> <button caption="Button"/> </layout>
In the following example, the auxiliary Label element (spacer) is used. Due to applied
expand
, it takes all the space left in the container.<layout expand="spacer"> <textField caption="Number"/> <dateField caption="Date"/> <label id="spacer"/> <hbox spacing="true"> <button caption="OK"/> <button caption="Cancel"/> </hbox> </layout>
4.3.1.2. Margins and Spacing
- Margin for screen borders
-
The margin attribute enables setting margins between container borders and nested components.
If
margin
is set totrue
, the margin is applied to all sides of the container.<layout> <vbox margin="true" height="100%"> <groupBox caption="Group" height="100%"/> </vbox> <groupBox caption="Group" height="100%"/> </layout>
Margins can be also set for each individual side (Top, Right, Bottom, Left). The example of top and bottom margins:
<vbox margin="true,false,true,false">
- Spacing between components
-
The spacing attribute indicates whether the space should be added between nested components in the direction of the container expansion.
TipSpacing will work correctly in case some of the nested components become invisible, so you should not use
margin
to emulate spacing.<layout spacing="true"> <button caption="Button"/> <button caption="Button"/> <button caption="Button"/> <button caption="Button"/> </layout>
4.3.1.3. Alignment
- Aligning components inside a container
-
Use the align attribute to align components within a container.
For example, here the label is located in the centre of the container:
<vbox height="100%"> <label align="MIDDLE_CENTER" value="Label"/> </vbox>
Component with specified alignment should not have 100% size in alignment direction. The container should provide more space than required by the component. The component will be aligned within this space.
The example of alignment within available space:
<vbox height="100%"> <label align="MIDDLE_CENTER" value="Label"/> </vbox>
4.3.1.4. Common Layout Mistakes
- Common mistake 1. Setting relative size for a component within a container with content-based size
-
Example of incorrect layout with relative size:
In this example, a
label
has 100% height, while the default height forVBox
is AUTO, i.e. content-based.Example of incorrect layout with expand:
Expand implicitly sets relative 100% height for the label, which is not correct, just like in the example above. In such cases, the screen may not look as expected. Some components may disappear or have zero size. If you encounter any layout problems, check that relative sizes are specified correctly first of all.
- Common mistake 2. Components inside a ScrollBox have 100% dimensions
-
Example of incorrect layout:
As a result of such mistake, scroll bars in
ScrollBox
will not appear even if the size of nested components exceeds the scrolling area.
- Common mistake 3. Aligning components with insufficient space
-
Example of incorrect layout:
In this example,
HBox
has content-dependent size, therefore the label alignment has no effect.
4.3.2. Passing Parameters to a Screen
Passing parameters from one screen to another is one of the most common tasks in UI development. Let’s consider this task taking an "order management" application as an example.
- Opening screen with openWindow
-
Parameters can be passed by providing them in a map to
openWindow()
,openLookup()
oropenEditor()
methods. They will be available in the opened screen as a map in the init() method and individually if you inject them with the @WindowParam annotation.Suppose we want to filter the products in the Product browser passing some parameters from the Order editor.
-
OrderEdit screen contains the
addOrderLine()
method which is invoked byaddOrderLine
action. The method opens a products lookup screen passing two parameters to it:-
the currently selected customer,
-
the list of already added products.
After a user selects a product, the QuantityDialog screen is opened for entering product quantity. When the user closes it, a new instance of OrderLine entity is created and added to the table.
openLookup("sample$Product.browse", items -> { if (!items.isEmpty()) { openQuantityDialog((Product) items.iterator().next()); } }, WindowManager.OpenType.THIS_TAB, ParamsMap.of( "customer", getItem().getCustomer(), "added", orderLinesDs.getItems().stream() .map(line -> line.getProduct().getId()) .collect(Collectors.toList()) ) );
-
-
ProductBrowse screen modifies its datasource query depending on passed customer. If a customer is provided, the table shows only products for this customer and those without reference to a customer. The parameters are injected in the screen controller using the @WindowParam annotation:
@WindowParam private Customer customer; @Override public void init(Map<String, Object> params) { if (customer != null) { productsDs.setQuery( "select e from sample$Product e left join e.customer c " + "where c.id = :param$customer or c is null"); } }
When the product browser is opened for looking up items to be used as order lines, it also creates and applies programmatically a filter to show only products not yet added to the order.
Tip@WindowParam private List<UUID> added; @Override public void ready() { if (added != null && !added.isEmpty()) { FilterEntity filterEntity = metadata.create(FilterEntity.class); filterEntity.setName("Not added yet"); filterEntity.setXml("<filter>\n" + " <and>\n" + " <c name=\"id\" class=\"java.util.UUID\" inExpr=\"true\" hidden=\"true\" operatorType=\"NOT_IN\" width=\"1\" type=\"PROPERTY\">" + " <![CDATA[((e.id not in :component$filter.id_list) or (e.id is null)) ]]>\n" + " <param name=\"component$filter.id_list\" javaClass=\"java.util.UUID\">NULL</param>\n" + " </c>\n" + " </and>\n" + "</filter>"); filter.setFilterEntity(filterEntity); filter.setParamValue("id_list", added); filter.apply(true); } }
The contents of the
FilterEntity.xml
attribute can be taken from a filter created at runtime: go to Entity Inspector, find the created filter which is stored as an instance of thesec$Filter
entity and copy its XML.
-
- Opening screen from a PickerField
-
The PickerField component and its ancestors can also pass parameters to opened screens. The parameters should be defined for the
PickerField
actions:LookupAction
orOpenAction
.Suppose we want to customize the caption of the Customer browser if it is opened from a
PickerField
in the Product editor.-
In the ProductEdit screen we set the parameters for the
PickerField
LookupAction using itssetLookupScreenParams()
method:public class ProductEdit extends AbstractEditor<Product> { @Named("fieldGroup.customer") private PickerField customerField; @Override protected void postInit() { customerField.getLookupAction().setLookupScreenParams(ParamsMap.of("product", getItem())); } }
-
Then, we inject the passed parameter in the CustomerBrowse screen:
@WindowParam private Product product; @Override public void init(Map<String, Object> params) { if (product != null && product.getName() != null) { getFrame().setCaption("Select a customer for " + product.getName()); } }
Now, if the customer browser is opened from the product editor, we will know exactly which product it was.
-
4.3.3. Returning Values from an Invoked Screen
The methods used for opening screens (openWindow()
, openLookup()
, openEditor()
) also allow you to get values back from these screens.
- Returning a value from lookup screen
-
The
openLookup()
method accepts a handler for the items selected in the opened lookup screen. In our example, the handler implemented by the lambda expression sets the selected customer for the editedOrder
instance.openLookup("sample$Customer.browse", items -> { if (!items.isEmpty()) { getItem().setCustomer((Customer) items.iterator().next()); } }, WindowManager.OpenType.DIALOG.setWidth("600px").setHeight("400px"));
- Returning a value from an arbitrary screen
-
The idea is that you return a reference to the controller of the opened screen, and then add a
CloseListener
to this reference. In the listener, you handle the values after the screen is closed.OrderEdit screen controller demonstrates two ways of looking up a Customer: from a lookup screen and from an arbitrary screen, both returning a
Customer
instance.The
openWindow()
method in the following example will open the customers list screen as a dialog window.CloseWithCommitListener
will be notified when the screen is closed by action withWindow.COMMIT_ACTION_ID
. This listener will be used to set the selected customer for the editedOrder
instance.CustomerList window = (CustomerList) openWindow("customer-list", WindowManager.OpenType.DIALOG); window.addCloseWithCommitListener(() -> { getItem().setCustomer(window.getSelectedCustomer()); });
4.3.4. One-to-One Composition with a Single Editor
It is often convenient to edit the One-to-One composition in one single editor. Let’s see how it can be implemented taking the Customer and the CustomerDetails relationship as an example.
-
customer-edit.xml descriptor contains the main
customerDs
and the nesteddetailsDs
datasources:<dsContext> <datasource id="customerDs" class="sample.entity.Customer" view="customer-view"> <datasource id="detailsDs" property="details"/> </datasource> </dsContext>
Fields for editing both entities are grouped into one
fieldGroup
, where some fields are bound to the nested datasource:<fieldGroup id="customerGroup" datasource="customerDs"> <column width="200px"> <field property="name"/> <field property="email"/> <field datasource="detailsDs" property="address" rows="3"/> <field datasource="detailsDs" property="note" rows="3"/> </column> </fieldGroup>
-
In the CustomerEdit controller we override the
initNewItem()
method. It will create aCustomerDetails
instance and link it to the newCustomer
instance when the latter is just created:@Inject private Metadata metadata; @Override protected void initNewItem(Customer customer) { customer.setDetails(metadata.create(CustomerDetails.class)); }
Finally, let’s handle the situation when a user clicks Create and then wants to close the editor without changing anything. This user will be asked for saving changes, as the
detailsDs
datasource already contains an empty instance andisModified()
method of theAbstractEditor
will always returntrue
. To prevent the appearance of confirmation dialog, we should makeisModified()
consider changes only in the master datasource:@Override public boolean isModified() { return customerDs.isModified(); }
Now, both linked entities can be created and edited in one editor screen.
4.3.5. Using Individual Fields instead of FieldGroup
Using the FieldGroup component in an entity’s editor is not mandatory. You can easily replace it with separate fields to create a custom screen layout.
Below is an example of such editor for the Order entity.
The order-edit.xml descriptor contains the main orderDs
and the nested orderLinesDs
datasources, as well as the independent customersDs
datasource for selecting a customer in a lookupField
:
<dsContext>
<datasource id="orderDs"
class="com.company.sample.entity.Order"
view="order-edit">
<collectionDatasource id="orderLinesDs"
property="orderLines"/>
</datasource>
<collectionDatasource id="customersDs"
class="com.company.sample.entity.Customer"
view="_minimal">
<query>
<![CDATA[select e from sample$Customer e]]>
</query>
</collectionDatasource>
</dsContext>
Now, all we have to do is to create the fields for the Order
attributes, bind them to the appropriate datasources and define the entity’s attribute using the property attribute:
-
the order’s number:
<textField id="numField" caption="msg://order.num" datasource="orderDs" property="num"/>
-
the order’s customer:
<lookupField id="customerField" caption="msg://order.customer" datasource="orderDs" property="customer" optionsDatasource="customersDs"/>
-
the order date:
<datePicker id="datePicker" caption="msg://order.date" datasource="orderDs" property="date"/>
-
the table containing the order lines:
<table id="orderLinesTable" height="300px" width="100%"> <rows datasource="orderLinesDs"/> . . . </table>
This approach is more flexible in terms of screen layout design. You can select any visual components for the fields, group the fields and place them wherever you like on the screen.
4.4. Loading and Saving Data
This section describes different ways of loading and saving data to the database.
4.4.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 |
You should obtain a reference to EntityManager through the Persistence interface. |
DataManager defines a few high-level methods for working with detached entities: |
EntityManager mostly resembles the standard |
DataManager in fact delegates to DataStore implementations, so the DataManager features listed below apply only to the most common case when you work with entities located in a relational database:
DataManager | EntityManager |
---|---|
DataManager always starts new transactions internally. |
You have to open a transaction before working with EntityManager. |
DataManager loads partial entities according to views. There are a few exceptions, see details here. |
EntityManager loads all local attributes. If a view is specified, it affects only reference attributes. See details here. |
DataManager executes only JPQL queries. Besides, it has separate methods for loading entities: |
EntityManager can run any JPQL or native (SQL) queries. |
DataManager checks security restrictions when invoked on the client tier. |
EntityManager does not impose security restrictions. |
When you work with data on the client tier, you have only one option - DataManager
. On the middleware, use EntityManager
when you need to implement some atomic logic inside a transaction or if the EntityManager interface is better suited to the task. Otherwise, on the middleware you can use both.
If you need to overcome restrictions of DataManager
when working on the client tier, create your own service and use EntityManager
to work with data. In the service, you can check permissions using the Security interface and return data to the client in the form of persistent or non-persistent entities or arbitrary values.
4.5. Using REST API
This section contains REST API usage examples.
The detailed information about REST API methods is written according to Swagger specification and is available at address http://files.cuba-platform.com/swagger/6.9.
4.5.1. Getting an OAuth Token
An OAuth token is required for any REST API method (except when you are using an anonymous access). A token can be obtained by the POST request on the address:
http://localhost:8080/app/rest/v2/oauth/token
An access to this endpoint is protected with a basic authentication. REST API client identifier and password is used for basic authentication. Please note that these are not an application user login and password. REST API client id and password are defined in the application properties cuba.rest.client.id and cuba.rest.client.secret (the default values are client
and secret
). You must pass the client id and secret, separated by a single colon (":") character, within a base64 encoded string in the Authorization
header.
The request type must be application/x-www-form-urlencoded
, the encoding is UTF-8
.
The request must contain the following parameters:
-
grant_type
-password
. -
username
- application user login. -
password
- application user password.
POST /oauth/token
Authorization: Basic Y2xpZW50OnNlY3JldA==
Content-Type: application/x-www-form-urlencoded
grant_type=password&username=smith&password=qwerty123
You can also use cURL:
curl -H "Content-type: application/x-www-form-urlencoded" -H "Authorization: Basic Y2xpZW50OnNlY3JldA==" -d "grant_type=password&username=admin&password=admin" http://localhost:8080/app/rest/v2/oauth/token
Method returns a JSON object:
{
"access_token": "29bc6b45-83cd-4050-8c7a-2a8a60adf251",
"token_type": "bearer",
"refresh_token": "e765446f-d49e-4634-a6d3-2d0583a0e7ea",
"expires_in": 43198,
"scope": "rest-api"
}
An access token value is in the access_token
property.
In order to use the access token, put it in the Authorization
header with the Bearer
type, for example:
Authorization: Bearer 29bc6b45-83cd-4050-8c7a-2a8a60adf251
The refresh_token
property contains a refresh token value. A refresh token cannot be used for accessing the protected resources, but it has a longer lifetime than an access token and it can be used to obtain new access token when the current one is expired.
The request for getting new access token using the refresh token must contain the following parameters:
-
grant_type
-refresh_token
. -
refresh_token
- a refresh token value.
POST /oauth/token
Authorization: Basic Y2xpZW50OnNlY3JldA==
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=e765446f-d49e-4634-a6d3-2d0583a0e7ea
See also the following application properties related to tokens:
4.5.2. REST API Authentication with LDAP
LDAP Authentication for REST can be enabled using the following properties:
-
cuba.rest.ldap.enabled
- whether LDAP authentication is enabled or not. -
cuba.rest.ldap.urls
– LDAP server URL. -
cuba.rest.ldap.base
– base DN for user search. -
cuba.rest.ldap.user
– the distinguished name of a system user which has the right to read the information from the directory. -
cuba.rest.ldap.password
– the password for the system user defined in the cuba.web.ldap.user property. -
cuba.rest.ldap.userLoginField
- the name of an LDAP user attribute that is used for matching the login name.sAMAccountName
by default (suitable for Active Directory).
Example of local.app.properties file:
cuba.rest.ldap.enabled = true
cuba.rest.ldap.urls = ldap://192.168.1.1:389
cuba.rest.ldap.base = ou=Employees,dc=mycompany,dc=com
cuba.rest.ldap.user = cn=System User,ou=Employees,dc=mycompany,dc=com
cuba.rest.ldap.password = system_user_password
You can obtain OAuth token using the following end-point:
http://localhost:8080/app/rest/v2/ldap/token
An access to this endpoint is protected with the basic authentication. REST API client identifier and password are used for basic authentication. Please note that these are not the application user login and password. REST API client id and password are defined in the application properties cuba.rest.client.id and cuba.rest.client.secret (the default values are client
and secret
). You must pass the client id and secret, separated by a single colon (":") character, within a base64 encoded string in the Authorization
header.
Request parameters are the same as for standard authentication:
-
grant_type
- alwayspassword
. -
username
- application user login. -
password
- application user password.
The request type must be application/x-www-form-urlencoded
, the encoding is UTF-8
.
Also, standard authentication with login and password can be disabled:
cuba.rest.standardAuthenticationEnabled = false
4.5.3. Custom Authentication
Authentication mechanisms can provide access tokens by key, link, LDAP login and password, etc. REST API uses its own authentication mechanism that cannot be modified. In order to use custom authentication process, you need to create a REST controller and use its URL.
Let’s consider the custom authentication mechanism that enables getting an OAuth token by a promo code. In the following example we will use a sample application that contains the Coupon
entity with code
attribute. We will send this attribute’s value as an authentication parameter in GET request.
-
Create a
Coupon
entity with thecode
attribute:@Column(name = "CODE", unique = true, length = 4) protected String code;
-
Create a user with promo-user login on behalf of which the authentication will be performed.
-
Create a new Spring configuration file with name
rest-dispatcher-spring.xml
under the root package (com.company.demo
) of web module. The content of the file must be as follows:<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd"> <context:component-scan base-package="com.company.demo.web.rest"/> </beans>
-
Include the file into the
cuba.restSpringContextConfig
application property in themodules/web/src/web-app.properties
file:cuba.restSpringContextConfig = +com/company/demo/rest-dispatcher-spring.xml
-
Create the
rest
package under the root package of web module and implement the custom Spring MVC controller in it. Use theOAuthTokenIssuer
bean to generate and issue the REST API token for a user after the custom authentication:@RestController @RequestMapping("auth-code") public class AuthCodeController { @Inject private OAuthTokenIssuer oAuthTokenIssuer; @Inject private LoginService loginService; @Inject private Configuration configuration; @Inject private DataManager dataManager; @Inject private MessageTools messageTools; // here we check secret code and issue token using OAuthTokenIssuer @RequestMapping(method = RequestMethod.GET) public ResponseEntity get(@RequestParam("code") String authCode) { // obtain system session to be able to call middleware services WebAuthConfig webAuthConfig = configuration.getConfig(WebAuthConfig.class); UserSession systemSession; try { systemSession = loginService.getSystemSession(webAuthConfig.getTrustedClientPassword()); } catch (LoginException e) { throw new RuntimeException("Error during system auth"); } // set security context AppContext.setSecurityContext(new SecurityContext(systemSession)); try { // find coupon with code LoadContext<Coupon> loadContext = LoadContext.create(Coupon.class) .setQuery(LoadContext.createQuery("select c from demo$Coupon c where c.code = :code") .setParameter("code", authCode)); if (dataManager.load(loadContext) == null) { // if coupon is not found - code is incorrect return new ResponseEntity<>(new ErrorInfo("invalid_grant", "Bad credentials"), HttpStatus.BAD_REQUEST); } // generate token for "promo-user" OAuthTokenIssuer.OAuth2AccessTokenResult tokenResult = oAuthTokenIssuer.issueToken("promo-user", messageTools.getDefaultLocale(), Collections.emptyMap()); OAuth2AccessToken accessToken = tokenResult.getAccessToken(); // set security HTTP headers to prevent browser caching of security token HttpHeaders headers = new HttpHeaders(); headers.set(HttpHeaders.CACHE_CONTROL, "no-store"); headers.set(HttpHeaders.PRAGMA, "no-cache"); return new ResponseEntity<>(accessToken, headers, HttpStatus.OK); } finally { // clean up security context AppContext.setSecurityContext(null); } } // POJO for JSON error messages public static class ErrorInfo implements Serializable { private String error; private String error_description; public ErrorInfo(String error, String error_description) { this.error = error; this.error_description = error_description; } public String getError() { return error; } public String getError_description() { return error_description; } } }
-
Exclude the
rest
package from scanning in web/core modules: theOAuthTokenIssuer
bean is available only in REST API context, and scanning for it in the application context will cause an error.<context:component-scan base-package="com.company.demo"> <context:exclude-filter type="regex" expression="com\.company\.demo\.web\.rest\..*"/> </context:component-scan>
-
Now users will be able to obtain OAuth2 access code using GET HTTP request with the
code
parameter tohttp://localhost:8080/app/rest/auth-code?code=A325
The result will be:
{"access_token":"74202587-6c2b-4d74-bcf2-0d687ea85dca","token_type":"bearer","expires_in":43199,"scope":"rest-api"}
The obtained access token should then be passed to REST API, as described in the documentation.
4.5.3.1. Social Login in REST API
The mechanism of social login can be used in REST API too. The complete sample application is available on GitHub and described in the Social Login section, below are the key points of getting an access token with a Facebook account.
-
Create the
restapi
package under the root package of web module and implement the custom Spring MVC controller in it. This controller should contain two main methods:get()
to get aResponseEntity
instance andlogin()
to obtain an OAuth token.@RequestMapping(method = RequestMethod.GET) public ResponseEntity get() { String loginUrl = getAsPrivilegedUser(() -> facebookService.getLoginUrl(getAppUrl(), OAuth2ResponseType.CODE_TOKEN) ); HttpHeaders headers = new HttpHeaders(); headers.set(HttpHeaders.LOCATION, loginUrl); return new ResponseEntity<>(headers, HttpStatus.FOUND); }
Here we check the Facebook code, obtain an access code and issue the access token using
OAuthTokenIssuer
:@RequestMapping(method = RequestMethod.POST, value = "login") public ResponseEntity<OAuth2AccessToken> login(@RequestParam("code") String code) { User user = getAsPrivilegedUser(() -> { FacebookUserData userData = facebookService.getUserData(getAppUrl(), code); return socialRegistrationService.findOrRegisterUser( userData.getId(), userData.getEmail(), userData.getName()); }); OAuth2AccessTokenResult tokenResult = oAuthTokenIssuer.issueToken(user.getLogin(), messageTools.getDefaultLocale(), Collections.emptyMap()); HttpHeaders headers = new HttpHeaders(); headers.set(HttpHeaders.CACHE_CONTROL, "no-store"); headers.set(HttpHeaders.PRAGMA, "no-cache"); return new ResponseEntity<>(tokenResult.getAccessToken(), headers, HttpStatus.OK); }
-
Exclude the
restapi
package from scanning in web/core modules: theOAuthTokenIssuer
bean is available only in REST API context, and scanning for it in the application context will cause an error.<context:component-scan base-package="com.company.demo"> <context:exclude-filter type="regex" expression="com\.company\.demo\.restapi\..*"/> </context:component-scan>
-
Create the
facebook-login-demo.html
file in themodules/web/web/VAADIN
folder of your project. It will contain the JavaScript code running on HTML page:<html> <head> <title>Facebook login demo with REST-API</title> <script src="jquery-3.2.1.min.js"></script> <style type="text/css"> #users { display: none; } </style> </head> <body> <h1>Facebook login demo with REST-API</h1> <script type="application/javascript"...> </script> <a id="fbLink" href="/app/rest/facebook">Login with Facebook</a> <div id="users"> You are logged in! <h1>Users</h1> <div id="usersList"> </div> </div> </body> </html>
The following script will try to login with Facebook. Firstly, it will remove code parameters from URL, then it will pass the code to REST API to get an OAuth access token, and in case of successful authentication we will be able to load and save data as usual.
var oauth2Token = null; function tryToLoginWithFacebook() { var urlHash = window.location.hash; if (urlHash && urlHash.indexOf('&code=') >= 0) { console.log("Try to login to CUBA REST-API!"); var urlCode = urlHash.substring(urlHash.indexOf('&code=') + '&code='.length); console.log("Facebook code: " + urlCode); history.pushState("", document.title, window.location.pathname); $.post({ url: '/app/rest/facebook/login', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, dataType: 'json', data: {code: urlCode}, success: function (data) { oauth2Token = data.access_token; loadUsers(); } }) } } function loadUsers() { $.get({ url: '/app/rest/v2/entities/sec$User?view=_local', headers: { 'Authorization': 'Bearer ' + oauth2Token, 'Content-Type': 'application/x-www-form-urlencoded' }, success: function (data) { $('#fbLink').hide(); $('#users').show(); $.each(data, function (i, user) { $('#usersList').append("<li>" + user.name + " (" + user.email + ")</li>"); }); } }); } tryToLoginWithFacebook();
Another example or running a JavaScript code from CUBA applications you can find in the JavaScript Usage Example section.
4.5.4. Getting an Entity Instances List
Let’s suppose that the system has a sales$Order
entity and we need to get a list of this entity instances. Besides, we need to get not all the records, but only 50 records, starting with the 100th one. A response must contain not only simple properties of the sales$Order
entity but also an information about the order customer (a reference field named customer
). Orders must be sorted by date.
A base URL for getting all instances of the sales$Order
entity is as follows:
http://localhost:8080/app/rest/v2/entities/sales$Order
To implement all the conditions described above the following request parameters must be specified:
-
view - a view, that will be used for loading entities. In our case the
order-edit-view
contains acustomer
reference. -
limit - a number of instances to be returned.
-
offset - a position of the first extracted record.
-
sort - an entity attribute name that will be used for sorting.
An OAuth token must be put in the Authorization
header with the Bearer
type:
Authorization: Bearer 29bc6b45-83cd-4050-8c7a-2a8a60adf251
As a result, we get the following GET request URL:
http://localhost:8080/app/rest/v2/entities/sales$Order?view=order-edit-view&limit=50&offset=100&sort=date
Using cURL, the request will look like:
curl -H "Authorization: Bearer d335902c-9cb4-455e-bf92-24ca1d66d72f" http://localhost:8080/app/rest/v2/entities/sales$Order?view=order-edit&limit=50&offset=100&sort=date
The response will be like this:
[
{
"_entityName": "sales$Order",
"_instanceName": "00001",
"id": "46322d73-2374-1d65-a5f2-160461da22bf",
"date": "2016-10-31",
"description": "Vacation order",
"number": "00001",
"items": [
{
"_entityName": "sales$OrderItem",
"_instanceName": "Beach umbrella",
"id": "95a04f46-af7a-a307-de4e-f2d73cfc74f7",
"price": 23,
"name": "Beach umbrella"
},
{
"_entityName": "sales$OrderItem",
"_instanceName": "Sun lotion",
"id": "a2129675-d158-9e3a-5496-41bf1a315917",
"price": 9.9,
"name": "Sun lotion"
}
],
"customer": {
"_entityName": "sales$Customer",
"_instanceName": "Toby Burns",
"id": "4aa9a9d8-01df-c8df-34c8-c385b566ea05",
"firstName": "Toby",
"lastName": "Burns"
}
},
{
"_entityName": "sales$Order",
"_instanceName": "00002",
"id": "b2ad3059-384c-3e03-b62d-b8c76621b4a8",
"date": "2016-12-31",
"description": "New Year party set",
"number": "00002",
"items": [
{
"_entityName": "sales$OrderItem",
"_instanceName": "Jack Daniels",
"id": "0c566c9d-7078-4567-a85b-c67a44f9d5fe",
"price": 50.7,
"name": "Jack Daniels"
},
{
"_entityName": "sales$OrderItem",
"_instanceName": "Hennessy X.O",
"id": "c01be87b-3f91-7a86-50b5-30f2f0a49127",
"price": 79.9,
"name": "Hennessy X.O"
}
],
"customer": {
"_entityName": "sales$Customer",
"_instanceName": "Morgan Collins",
"id": "5d111245-2ed0-abec-3bee-1a196da92e3e",
"firstName": "Morgan",
"lastName": "Collins"
}
}
]
Please note, that every entity in the response has a _entityName
attribute with the entity name and an _instanceName
attribute with the entity instance name.
4.5.5. New Entity Instance Creation
New sales$Order
entity instance can be created with the POST request on the address:
http://localhost:8080/app/rest/v2/entities/sales$Order
An OAuth token must be put in the Authorization
header with the Bearer
type.
The request body must contain a JSON object that describes a new entity instance, e.g.:
{
"number": "00017",
"date": "2016-09-01",
"description": "Back to school",
"items": [
{
"_entityName": "sales$OrderItem",
"price": 100,
"name": "School bag"
},
{
"_entityName": "sales$OrderItem",
"price": 9.90,
"name": "Pencils"
}
],
"customer": {
"id": "4aa9a9d8-01df-c8df-34c8-c385b566ea05"
}
}
Below is an example of cURL POST request that creates a new Order
instance:
curl -H "Authorization: Bearer d335902c-9cb4-455e-bf92-24ca1d66d72f" -H "Content-Type: application/json" -X POST -d "{\"date\": \"2018-10-12 15:47:28\", \"amount\": 9.90, \"customer\": {\"id\": \"383ebce2-b295-7378-36a1-bcf93693821f\"}}" http://localhost:8080/app/rest/v2/entities/sales$Order
A collection of order items (items
) and a customer
reference are passed in the request body. Let’s examine how these attributes will be processed.
First, let’s have a quick look to the Order
class:
package com.company.sales.entity;
import com.haulmont.chile.core.annotations.Composition;
import com.haulmont.chile.core.annotations.NamePattern;
import com.haulmont.cuba.core.entity.StandardEntity;
import com.haulmont.cuba.core.entity.annotation.OnDelete;
import com.haulmont.cuba.core.global.DeletePolicy;
import javax.persistence.*;
import java.util.Date;
import java.util.Set;
@NamePattern("%s|number")
@Table(name = "SALES_ORDER")
@Entity(name = "sales$Order")
public class Order extends StandardEntity {
private static final long serialVersionUID = 7565070704618724997L;
@Column(name = "NUMBER_")
protected String number;
@Temporal(TemporalType.DATE)
@Column(name = "DATE_")
protected Date date;
@Column(name = "DESCRIPTION")
protected String description;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "CUSTOMER_ID")
protected Customer customer;
@Composition
@OnDelete(DeletePolicy.CASCADE)
@OneToMany(mappedBy = "order")
protected Set<OrderItem> items;
//getters and setters omitted
}
The items
collection property is annotated with the @Composition. REST API methods for entity creation and update will create a new entity instances for all members of such collections. In our case, two instances of OrderItem
entity will be created with the Order
entity.
The customer
reference doesn’t have a @Composition
annotation, that’s why the REST API will try to find a client with the given id and set it to the customer
field. If the client is not found then an order won’t be created and the method will return an error.
In case of successful method execution a full object graph of the created entity is returned:
{
"_entityName": "sales$Order",
"id": "5d7ff8e3-7828-ba94-d6ba-155c5c4f2a50",
"date": "2016-09-01",
"description": "Back to school",
"version": 1,
"number": "00017",
"createdBy": "admin",
"createTs": "2016-10-13 18:12:21.047",
"updateTs": "2016-10-13 18:12:21.047",
"items": [
{
"_entityName": "sales$OrderItem",
"id": "3158b8ed-7b7a-568e-aec5-0822c3ebbc24",
"createdBy": "admin",
"price": 9.9,
"name": "Pencils",
"createTs": "2016-10-13 18:12:21.047",
"version": 1,
"updateTs": "2016-10-13 18:12:21.047",
"order": {
"_entityName": "sales$Order",
"id": "5d7ff8e3-7828-ba94-d6ba-155c5c4f2a50"
}
},
{
"_entityName": "sales$OrderItem",
"id": "72774b8b-4fea-6403-7b52-4a6a749215fc",
"createdBy": "admin",
"price": 100,
"name": "School bag",
"createTs": "2016-10-13 18:12:21.047",
"version": 1,
"updateTs": "2016-10-13 18:12:21.047",
"order": {
"_entityName": "sales$Order",
"id": "5d7ff8e3-7828-ba94-d6ba-155c5c4f2a50"
}
}
],
"customer": {
"_entityName": "sales$Customer",
"id": "4aa9a9d8-01df-c8df-34c8-c385b566ea05",
"firstName": "Toby",
"lastName": "Burns",
"createdBy": "admin",
"createTs": "2016-10-13 15:32:01.657",
"version": 1,
"updateTs": "2016-10-13 15:32:01.657"
}
}
4.5.6. Existing Entity Instance Update
An existing sales$Order
entity instance can be updated with the PUT request on the address:
http://localhost:8080/app/rest/v2/entities/sales$Order/5d7ff8e3-7828-ba94-d6ba-155c5c4f2a50
The last part of the query here is the entity identifier.
An OAuth token must be put in the Authorization
header with the Bearer
type.
The request body must contain a JSON object containing only fields we want to update, e.g.:
{
"date": "2017-10-01",
"customer" : {
"id" : "5d111245-2ed0-abec-3bee-1a196da92e3e"
}
}
The response body will contain a modified entity:
{
"_entityName": "sales$Order",
"id": "5d7ff8e3-7828-ba94-d6ba-155c5c4f2a50",
"date": "2017-10-01",
"updatedBy": "admin",
"description": "Back to school",
"version": 2,
"number": "00017",
"createdBy": "admin",
"createTs": "2016-10-13 18:12:21.047",
"updateTs": "2016-10-13 19:13:02.656",
"customer": {
"_entityName": "sales$Customer",
"id": "5d111245-2ed0-abec-3bee-1a196da92e3e",
"firstName": "Morgan",
"lastName": "Collins",
"createdBy": "admin",
"createTs": "2016-10-13 15:31:27.821",
"version": 1,
"updateTs": "2016-10-13 15:31:27.821",
"email": "collins@gmail.com"
}
}
4.5.7. Executing a JPQL Query (GET)
Before the execution with the REST API a query must be described in the configuration file. The rest-queries.xml
file must be created in the main package of the web module (e.g. com.company.sales
). Then the file must be defined in the application properties file of the web module (web-app.properties).
cuba.rest.queriesConfig = +com/company/sales/rest-queries.xml
rest-queries.xml
contents:
<?xml version="1.0"?>
<queries xmlns="http://schemas.haulmont.com/cuba/rest-queries.xsd">
<query name="ordersAfterDate" entity="sales$Order" view="order-edit-view">
<jpql><![CDATA[select o from sales$Order o where o.date >= :startDate and o.date <= :endDate]]></jpql>
<params>
<param name="startDate" type="java.util.Date"/>
<param name="endDate" type="java.util.Date"/>
</params>
</query>
</queries>
To execute a JPQL query the following GET request must be executed:
http://localhost:8080/app/rest/v2/queries/sales$Order/ordersAfterDate?startDate=2016-11-01&endDate=2017-11-01
The request URL parts:
-
sales$Order
- extracted entity name. -
ordersAfterDate
- a query name from the configuration file. -
startDate
andendDate
- request parameters with the values.
An OAuth token must be put in the Authorization
header with the Bearer
type.
The method returns a JSON array of extracted entity instances:
[
{
"_entityName": "sales$Order",
"_instanceName": "00002",
"id": "b2ad3059-384c-3e03-b62d-b8c76621b4a8",
"date": "2016-12-31",
"description": "New Year party set",
"number": "00002",
"items": [
{
"_entityName": "sales$OrderItem",
"_instanceName": "Jack Daniels",
"id": "0c566c9d-7078-4567-a85b-c67a44f9d5fe",
"price": 50.7,
"name": "Jack Daniels"
},
{
"_entityName": "sales$OrderItem",
"_instanceName": "Hennessy X.O",
"id": "c01be87b-3f91-7a86-50b5-30f2f0a49127",
"price": 79.9,
"name": "Hennessy X.O"
}
],
"customer": {
"_entityName": "sales$Customer",
"_instanceName": "Morgan Collins",
"id": "5d111245-2ed0-abec-3bee-1a196da92e3e",
"firstName": "Morgan",
"lastName": "Collins"
}
}
]
A full list of possible request parameters is available in the Swagger documentation.
4.5.8. Executing a JPQL Query (POST)
It is also possible to execute a query with POST HTTP request. POST request can be used when you need to pass a collection as query parameter value. In this case, the type of the query parameter in REST queries configuration file must end with square brackets: java.lang.String[]
, java.util.UUID[]
, etc.
<?xml version="1.0"?>
<queries xmlns="http://schemas.haulmont.com/cuba/rest-queries.xsd">
<query name="ordersByIds" entity="sales$Order" view="order-edit-view">
<jpql><![CDATA[select o from sales$Order o where o.id in :ids and o.status = :status]]></jpql>
<params>
<param name="ids" type="java.util.UUID[]"/>
<param name="status" type="java.lang.String"/>
</params>
</query>
</queries>
Query parameters values must be passed in the request body as JSON map:
{
"ids": ["c273fca1-33c2-0229-2a0c-78bc6d09110a", "e6c04c18-c8a1-b741-7363-a2d58589d800", "d268a4e1-f316-a7c8-7a96-87ba06afbbbd"],
"status": "ready"
}
The POST request URL:
http://localhost:8080/app/rest/v2/queries/sales$Order/ordersByIds?returnCount=true
4.5.9. Service Method Invocation (GET)
Suppose there is an OrderService
service in the system. The implementation looks as follows:
package com.company.sales.service;
import com.haulmont.cuba.core.EntityManager;
import com.haulmont.cuba.core.Persistence;
import com.haulmont.cuba.core.Transaction;
import org.springframework.stereotype.Service;
import javax.inject.Inject;
import java.math.BigDecimal;
@Service(OrderService.NAME)
public class OrderServiceBean implements OrderService {
@Inject
private Persistence persistence;
@Override
public BigDecimal calculatePrice(String orderNumber) {
BigDecimal orderPrice = null;
try (Transaction tx = persistence.createTransaction()) {
EntityManager em = persistence.getEntityManager();
orderPrice = (BigDecimal) em.createQuery("select sum(oi.price) from sales$OrderItem oi where oi.order.number = :orderNumber")
.setParameter("orderNumber", orderNumber)
.getSingleResult();
tx.commit();
}
return orderPrice;
}
}
Before the execution with the REST API a service method invocation must be allowed in the configuration file. The rest-services.xml
file must be created in the main package of the web module (e.g. com.company.sales
). Then the file must be defined in the application properties file of the web module (web-app.properties).
cuba.rest.servicesConfig = +com/company/sales/rest-services.xml
rest-services.xml
content:
<?xml version="1.0" encoding="UTF-8"?>
<services xmlns="http://schemas.haulmont.com/cuba/rest-services-v2.xsd">
<service name="sales_OrderService">
<method name="calculatePrice">
<param name="orderNumber"/>
</method>
</service>
</services>
To invoke the service method the following GET request must be executed:
http://localhost:8080/app/rest/v2/services/sales_OrderService/calculatePrice?orderNumber=00001
The request URL parts:
-
sales_OrderService
- a service name. -
calculatePrice
- a method name. -
orderNumber
- an argument name with the value.
An OAuth token must be put in the Authorization
header with the Bearer
type.
A service method may return a result of simple datatype, an entity, an entities collection or a serializable POJO. In our case a BigDecimal is returned, so the response body contains just a number:
39.2
4.5.10. Service Method Invocation (POST)
REST API allows execution not only of methods that have arguments of simple datatypes, but also of methods with the following arguments:
-
entities
-
entities collections
-
serializable POJOs
Suppose we added a new method to the OrderService
created in the previous section:
@Override
public OrderValidationResult validateOrder(Order order, Date validationDate){
OrderValidationResult result=new OrderValidationResult();
result.setSuccess(false);
result.setErrorMessage("Validation of order "+order.getNumber()+" failed. validationDate parameter is: "+validationDate);
return result;
}
OrderValidationResult
class looks as follows:
package com.company.sales.service;
import java.io.Serializable;
public class OrderValidationResult implements Serializable {
private boolean success;
private String errorMessage;
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public String getErrorMessage() {
return errorMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
}
The new method has an Order
entity in the arguments list and returns a POJO.
Before the invocation with the REST API the method must be allowed, so we add a record to the rest-services.xml
configuration file (it was described in the Service Method Invocation (GET)).
<?xml version="1.0" encoding="UTF-8"?>
<services xmlns="http://schemas.haulmont.com/cuba/rest-services-v2.xsd">
<service name="sales_OrderService">
<method name="calculatePrice">
<param name="orderNumber"/>
</method>
<method name="validateOrder">
<param name="order"/>
<param name="validationDate"/>
</method>
</service>
</services>
The validateOrder
service method may be called with the POST request on the address:
http://localhost:8080/app/rest/v2/services/sales_OrderService/validateOrder
In case of the POST request parameters are passed in the request body. The request body must contain a JSON object, each field of this object corresponds to the service method argument.
{
"order" : {
"number": "00050",
"date" : "2016-01-01"
},
"validationDate": "2016-10-01"
}
An OAuth token must be put in the Authorization
header with the Bearer
type.
The REST API method returns a serialized POJO:
{
"success": false,
"errorMessage": "Validation of order 00050 failed. validationDate parameter is: 2016-10-01"
}
4.5.11. Files Downloading
When downloading a file, passing a security token in the request header is often inconvenient. It is desirable to have a URL for downloading that may be put to the src attribute of the img tag.
As a solution, an OAuth token can also be passed in the request URL as a parameter with the access_token name.
For example, an image is uploaded to the application. Its FileDescriptor id is 44809679-e81c-e5ae-dd81-f56f223761d6
.
In this case a URL for downloading the image will look like this:
http://localhost:8080/app/rest/v2/files/44809679-e81c-e5ae-dd81-f56f223761d6?access_token=a2f0bb4e-773f-6b59-3450-3934cbf0a2d6
4.5.12. Files Uploading
In order to upload a file, you should get an access token which will be used in the subsequent requests.
Suppose we have the following form for the file input:
<form id="fileForm">
<h2>Select a file:</h2>
<input type="file" name="file" id="fileUpload"/>
<br/>
<button type="submit">Upload</button>
</form>
<h2>Result:</h2>
<img id="uploadedFile" src="" style="display: none"/>
We will use jQuery for the upload and get a JSON with data which is the newly created FileDescriptor
instance. We can access the uploaded file by its FileDescriptor
id with the access token as a parameter:
$('#fileForm').submit(function (e) {
e.preventDefault();
var file = $('#fileUpload')[0].files[0];
var url = 'http://localhost:8080/app/rest/v2/files?name=' + file.name; // send file name as parameter
$.ajax({
type: 'POST',
url: url,
headers: {
'Authorization': 'Bearer ' + oauthToken // add header with access token
},
processData: false,
contentType: false,
dataType: 'json',
data: file,
success: function (data) {
alert('Upload successful');
$('#uploadedFile').attr('src',
'http://localhost:8080/app/rest/v2/files/' + data.id + '?access_token=' + oauthToken); // update image url
$('#uploadedFile').show();
}
});
});
4.5.13. JavaScript Usage Example
This section contains an example of using REST API v2 from JavaScript running on a HTML page. The page initially shows login form, and after successful login displays a message and a list of entities.
For simplicity, we will use modules/web/web/VAADIN
folder for storing HTML/CSS/JavaScript files, as the corresponding folder in the deployed web application is used for serving static resources by default. So you will not need to make any configuration of your Tomcat application server. The resulting URL will start from http://localhost:8080/app/VAADIN
, so do not use this approach in a real world application - create a separate web application with its own context instead.
Download jQuery and Bootstrap and copy to modules/web/web/VAADIN
folder of your project. Create customers.html
and customers.js
files, so the content of the folder should look as follows:
bootstrap.min.css
customers.html
customers.js
jquery-3.1.1.min.js
customers.html
file content:
<html>
<head>
<script type="text/javascript" src="jquery-3.1.1.min.js"></script>
<link rel="stylesheet" href="bootstrap.min.css"/>
</head>
<body>
<div style="width: 300px; margin: auto;">
<h1>Sales</h1>
<div id="loggedInStatus" style="display: none" class="alert alert-success">
Logged in successfully
</div>
<div id="loginForm">
<div class="form-group">
<label for="loginField">Login:</label>
<input type="text" class="form-control" id="loginField">
</div>
<div class="form-group">
<label for="passwordField">Password:</label>
<input type="password" class="form-control" id="passwordField">
</div>
<button type="submit" class="btn btn-default" onclick="login()">Submit</button>
</div>
<div id="customers" style="display: none">
<h2>Customers</h2>
<ul id="customersList"></ul>
</div>
</div>
<script type="text/javascript" src="customers.js"></script>
</body>
</html>
customers.js
file content:
var oauthToken = null;
function login() {
var userLogin = $('#loginField').val();
var userPassword = $('#passwordField').val();
$.post({
url: 'http://localhost:8080/app/rest/v2/oauth/token',
headers: {
'Authorization': 'Basic Y2xpZW50OnNlY3JldA==',
'Content-Type': 'application/x-www-form-urlencoded'
},
dataType: 'json',
data: {grant_type: 'password', username: userLogin, password: userPassword},
success: function (data) {
oauthToken = data.access_token;
$('#loggedInStatus').show();
$('#loginForm').hide();
loadCustomers();
}
})
}
function loadCustomers() {
$.get({
url: 'http://localhost:8080/app/rest/v2/entities/sales$Customer?view=_local',
headers: {
'Authorization': 'Bearer ' + oauthToken,
'Content-Type': 'application/x-www-form-urlencoded'
},
success: function (data) {
$('#customers').show();
$.each(data, function (i, customer) {
$('#customersList').append("<li>" + customer.name + " (" + customer.email + ")</li>");
});
}
});
}
Login and password from the user input are sent to the server by the POST request with the Base64-encoded client credentials in the Authorization
header as explained in Getting an OAuth Token section. If the authentication is successful, the web page receives an access token value from the server, the token is stored in the oauthToken
variable, the loginForm
div is hidden and the loggedInStatus
div is shown.
To show the list of customers, the request is sent to the server to get the instances of the sales$Customer
entity, passing the oauthToken
value in the Authorization
header.
In case the request is processed successfully, the customers
div is shown, and the customersList
element is filled with items containing customer names and emails.
4.5.14. Getting Localized Messages
There are methods in the REST API for getting localized messages for entities, their properties and enums.
For example, to get a list of localized messages for the sec$User
entity you have to execute the following GET request:
http://localhost:8080/app/rest/v2/messages/entities/sec$User
An OAuth token must be put in the Authorization
header with the Bearer
type.
You can explicitly specify the desired locale using the Accept-Language http header.
The response will be like this:
{
"sec$User": "User",
"sec$User.active": "Active",
"sec$User.changePasswordAtNextLogon": "Change Password at Next Logon",
"sec$User.createTs": "Created At",
"sec$User.createdBy": "Created By",
"sec$User.deleteTs": "Deleted At",
"sec$User.deletedBy": "Deleted By",
"sec$User.email": "Email",
"sec$User.firstName": "First Name",
"sec$User.group": "Group",
"sec$User.id": "ID",
"sec$User.ipMask": "Permitted IP Mask",
"sec$User.language": "Language",
"sec$User.lastName": "Last Name",
"sec$User.login": "Login",
"sec$User.loginLowerCase": "Login",
"sec$User.middleName": "Middle Name",
"sec$User.name": "Name",
"sec$User.password": "Password",
"sec$User.position": "Position",
"sec$User.substitutions": "Substitutions",
"sec$User.timeZone": "Time Zone",
"sec$User.timeZoneAuto": "Autodetect Time Zone",
"sec$User.updateTs": "Updated At",
"sec$User.updatedBy": "Updated By",
"sec$User.userRoles": "User Roles",
"sec$User.version": "Version"
}
To get the localization for enum, use the following URL:
http://localhost:8080/app/rest/v2/messages/enums/com.haulmont.cuba.security.entity.RoleType
If you omit the entity name or enum name part in the URL, you’ll get the localization for all entities or enums.
4.5.15. Data Model Versioning Example
- Entity attribute was renamed
-
Let’s suppose that the
oldNumber
attribute of thesales$Order
entity was renamed tonewNumber
anddate
was renamed todeliveryDate
. In this case transformation config will be like this:<?xml version="1.0"?> <transformations xmlns="http://schemas.haulmont.com/cuba/rest-json-transformations.xsd"> <transformation modelVersion="1.0" currentEntityName="sales$Order"> <renameAttribute oldName="oldNumber" currentName="newNumber"/> <renameAttribute oldName="date" currentName="deliveryDate"/> </transformation> ... </transformations>
If the client app needs to work with the old version of the
sales$Order
entity then it must pass themodelVersion
value in the URL parameter:http://localhost:8080/app/rest/v2/entities/sales$Order/c838be0a-96d0-4ef4-a7c0-dff348347f93?modelVersion=1.0
The following result will be returned:
{ "_entityName": "sales$Order", "_instanceName": "00001", "id": "46322d73-2374-1d65-a5f2-160461da22bf", "date": "2016-10-31", "description": "Vacation order", "oldNumber": "00001" }
The response JSON contains an
oldNumber
anddate
attributes although the entity in the CUBA application hasnewNumber
anddeliveryDate
attributes. - Entity name was changed
-
Next, let’s imagine, that in some next release of the application a name of the
sales$Order
entity was also changed. The new name issales$NewOrder
.Transformation config for version
1.1
will be like this:<?xml version="1.0"?> <transformations xmlns="http://schemas.haulmont.com/cuba/rest-json-transformations.xsd"> <transformation modelVersion="1.1" oldEntityName="sales$Order" currentEntityName="sales$NewOrder"> <renameAttribute oldName="oldNumber" currentName="newNumber"/> </transformation> ... </transformations>
In addition to the config from the previous example an
oldEntityName
attribute is added here. It specifies the entity name that was valid for model version1.1
. ThecurrentEntityName
attribute specifies the current entity name.Although an entity with a name
sales$Order
doesn’t exist anymore, the following request will work:http://localhost:8080/app/rest/v2/entities/sales$Order/c838be0a-96d0-4ef4-a7c0-dff348347f93?modelVersion=1.1
The REST API controller will understand that it must search among
sales$NewOrder
entities and after the entity with given id is found names of the entity and of thenewNumber
attribute will be replaced in the result JSON:{ "_entityName": "sales$Order", "_instanceName": "00001", "id": "46322d73-2374-1d65-a5f2-160461da22bf", "date": "2016-10-31", "description": "Vacation order", "oldNumber": "00001" }
The client app can also use the old version of data model for entity update and creation.
This POST request that uses old entity name and has old JSON in the request body will work:
http://localhost:8080/app/rest/v2/entities/sales$Order
{ "_entityName": "sales$Order", "_instanceName": "00001", "id": "46322d73-2374-1d65-a5f2-160461da22bf", "date": "2016-10-31", "description": "Vacation order", "oldNumber": "00001" }
- Entity attribute must be removed from JSON
-
If some attribute was added to the entity, but the client that works with the old version of data model doesn’t expect this new attribute, then the new attribute can be removed from the result JSON.
Transformation configuration for this case will look like this:
<?xml version="1.0"?> <transformations xmlns="http://schemas.haulmont.com/cuba/rest-json-transformations.xsd"> <transformation modelVersion="1.5" currentEntityName="sales$Order"> <toVersion> <removeAttribute name="discount"/> </toVersion> </transformation> ... </transformations>
Transformation in this config file contains a
toVersion
tag with a nestedremoveAttribute
command. This means that when the transformation from the current state to specific version is performed (i.e. when you request a list of entities) then adiscount
attribute must be removed from the result JSON.In this case if you perform the request without the
modelVersion
attribute, the discount attribute will be returned:http://localhost:8080/app/rest/v2/entities/sales$Order/c838be0a-96d0-4ef4-a7c0-dff348347f93
{ "_entityName": "sales$Order", "_instanceName": "00001", "id": "46322d73-2374-1d65-a5f2-160461da22bf", "deliveryDate": "2016-10-31", "description": "Vacation order", "number": "00001", "discount": 50 }
If you specify the
modelVersion
thendiscount
attribute will be removedhttp://localhost:8080/app/rest/v2/entities/sales$Order/c838be0a-96d0-4ef4-a7c0-dff348347f93?modelVersion=1.1
{ "_entityName": "sales$Order", "_instanceName": "00001", "id": "46322d73-2374-1d65-a5f2-160461da22bf", "deliveryDate": "2016-10-31", "description": "Vacation order", "oldNumber": "00001" }
- Using custom transformer
-
You can also create and register a custom JSON transformer. As an example let’s examine the following situation: there was an entity
sales$OldOrder
that was renamed tosales$NewOrder
. This entity has anorderDate
field. In the previous version, this date field contained a time part, but in the latest version of the entity, the time part is removed. REST API client that request the entity with an old model version1.0
expects the date field to have the time part, so the transformer must modify the value in the JSON.First, that’s how the transformer configuration must look like:
<?xml version="1.0"?> <transformations xmlns="http://schemas.haulmont.com/cuba/rest-json-transformations.xsd"> <transformation modelVersion="1.0" oldEntityName="sales$OldOrder" currentEntityName="sales$NewOrder"> <custom> <fromVersion transformerBeanRef="sales_OrderJsonTransformerFromVersion"/> <toVersion transformerBeanRef="sales_OrderJsonTransformerToVersion"/> </custom> </transformation> ... </transformations>
There are a
custom
element and nestedtoVersion
andfromVersion
elements. These elements have a reference to the transformer bean. This means that custom transformer must be registered as a Spring bean. There is one important thing here: a custom transformer may use theRestTransformations
platform bean (this bean gives an access to other entities transformers if it is required). But theRestTransformations
bean is registered in the Spring context of the REST API servlet, not in the main context of the web application. This means that custom transformer beans must be registered in the REST API Spring context as well.That’s how we can do that.
First, create a
rest-dispatcher-spring.xml
in the web or portal module (e.g. in packagecom.company.test
).Next, register this file in the
app.properties
of the web or portal module:cuba.restSpringContextConfig = +com/company/test/rest-dispatcher-spring.xml
The
rest-dispatcher-spring.xml
must contain custom transformer bean definitions:<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd"> <bean name="sales_OrderJsonTransformerFromVersion" class="com.company.test.transformer.OrderJsonTransformerFromVersion"/> <bean name="sales_OrderJsonTransformerToVersion" class="com.company.test.transformer.OrderJsonTransformerToVersion"/> </beans>
The content of the
sales_OrderJsonTransformerToVersion
transformer is as follows:package com.company.test.transformer; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.base.Strings; import com.haulmont.restapi.transform.AbstractEntityJsonTransformer; import com.haulmont.restapi.transform.JsonTransformationDirection; public class OrderJsonTransformerToVersion extends AbstractEntityJsonTransformer { public OrderJsonTransformerToVersion() { super("sales$NewOrder", "sales$OldOrder", "1.0", JsonTransformationDirection.TO_VERSION); } @Override protected void doCustomTransformations(ObjectNode rootObjectNode, ObjectMapper objectMapper) { JsonNode orderDateNode = rootObjectNode.get("orderDate"); if (orderDateNode != null) { String orderDateNodeValue = orderDateNode.asText(); if (!Strings.isNullOrEmpty(orderDateNodeValue)) rootObjectNode.put("orderDate", orderDateNodeValue + " 00:00:00.000"); } } }
This transformer finds the
orderDate
node in the JSON object and modifies its value by adding the time part to the value.When the
sales$OldOrder
entity with a data model version1.0
is requested, the result JSON will contain entities withorderDate
fields that contain time part, although it is not stored in the database anymore.A couple more words about custom transformers. They must implement the
EntityJsonTransformer
interface. You can also extend theAbstractEntityJsonTransformer
class and override itsdoCustomTransformations
method. TheAbstractEntityJsonTransformer
contains all functionality of the standard transformer.
4.5.16. Using Entities Search Filter
REST API allows you to specify ad-hoc search criteria when getting a list of entities.
Let’s suppose that we have two entities:
-
Author that has two fields:
lastName
andfirstName
-
Book with three fields:
title
(String),author
(Author) andpublicationYear
(Integer)
To perform a search with conditions we must use the URL like this:
http://localhost:8080/app/rest/v2/entities/test$Book/search
The search conditions must be passed in the filter
parameter. It is a JSON object that contains a set of conditions. If the search is performed with the GET request, then the filter
parameter must be passed in the URL.
- Example 1
-
We need to find all books that were released in 2007 and have an author with the first name starting with "Alex". The filter JSON should look like this:
{
"conditions": [
{
"property": "author.firstName",
"operator": "startsWith",
"value": "Alex"
},
{
"property": "publicationDate",
"operator": "=",
"value": 2007
}
]
}
By default, search criteria are applied with the AND operation.
This example also demonstrates that nested properties are supported (author.firstName
).
- Example 2
-
The next example demonstrates two things: how to execute a search with the POST request and how to use OR groups. In case of POST request all parameters must be passed in the JSON object that is passed in the request body. The search filter must be placed in the object field called
filter
. All other parameters (view name, limit, etc.) must be placed in fields with corresponding names:
{
"filter": {
"conditions": [
{
"group": "OR",
"conditions": [
{
"property": "author.lastName",
"operator": "contains",
"value": "Stev"
},
{
"property": "author.lastName",
"operator": "=",
"value": "Dumas"
}
]
},
{
"property": "publicationDate",
"operator": "=",
"in": [2007, 2008]
}
]
},
"view": "book-view"
}
In this example, conditions
collection contains not only condition objects, but also an OR group. So the result search criterion will be:
((author.lastName contains Stev) OR (author.lastName = Duma) AND (publicationDate in [2007, 2008]))
Notice that the view
parameter is also passed in the request body.
4.6. Working with Application Components
Any CUBA application can be used as a component of another application. An application component is a full-stack library providing functionality on all layers - from database schema to business logic and UI.
Application components published on CUBA Marketplace are called add-ons, because they extend functionality of the framework and can be used in any CUBA-based application.
4.6.1. Using Public Add-ons
An add-on published on Marketplace can be added to your project in one of the ways described below. The first and the second approaches assume that you use one of the standard CUBA repositories. The last approach is applicable for open-source add-ons and doesn’t involve any remote repositories.
- By Studio
-
-
Edit Project properties and on the App components panel click the plus button next to Custom components.
-
Copy add-on coordinates from the marketplace page or from the add-on’s documentation and paste them in the coordinates field, for example:
com.haulmont.addon.cubajm:cuba-jm-global:0.3.1
-
Click OK in the dialog. Studio will try to find the add-on binaries in the repository currently selected for the project. If it is found, the dialog will close and the add-on will appear in the list of custom components.
-
Save the project properties by clicking OK.
-
- By manual editing
-
-
Edit
build.gradle
and specify the add-on coordinates in the rootdependencies
section:dependencies { appComponent("com.haulmont.cuba:cuba-global:$cubaVersion") // your add-ons go here appComponent("com.haulmont.addon.cubajm:cuba-jm-global:0.3.1") }
-
Execute
gradlew idea
in the command line to include add-on in your project’s development environment. -
Edit
web.xml
files of thecore
andweb
modules and add the add-on identifier (which is equal to Maven groupId) to the space-separated list of application components in theappComponents
context parameter:<context-param> <param-name>appComponents</param-name> <param-value>com.haulmont.cuba com.haulmont.addon.cubajm</param-value> </context-param>
-
- By building from sources
-
-
Clone the add-on’s repository to a local directory and import the project into Studio.
-
Execute Run > Install app component main menu command to install the add-on to the local Maven repository (by default it is
~/.m2
directory). -
Open your project in Studio and select the Use local Maven repository checkbox on the Advanced tab of the Project properties page.
-
On the App components panel, click the plus button next to Custom components and select the add-on in the drop-down list at the bottom of the dialog. The add-on’s coordinates will be shown in the field at the top.
-
Click OK in the dialog and save the project properties.
-
4.6.2. Creating Application Components
This section contains some recommendations useful if you are developing a reusable application component.
- Naming rules
-
-
Choose the root package using the standard reverse-DNS notation, e.g.
com.jupiter.amazingsearch
.Root package should not begin with a root package of any other component or application. For example, if you have an application with
com.jupiter.tickets
root package, you cannot usecom.jupiter.tickets.amazingsearch
package for a component. The reason is that Spring scans the classpath for the beans starting from the specified root package, and this scanning space must be unique for each component. -
Namespace is used as a prefix for the database tables, so for a public component it should be composite, like
jptams
, not justsearch
. It will minimize the risk of name collisions in the target application. You cannot use underscores and dashes in namespace, only letters and digits. -
Module prefix should repeat namespace, but can contain dashes, like
jpt-amsearch
. -
Use namespace as a prefix for bean names and application properties, for example:
@Component("jptams_Finder") @Property("jptams.ignoreCase")
-
- Installing into the local Maven repository
-
In order to make the component available to the projects located on your computer, install it into the local Maven repository by executing the Run > Install app component menu command. This command just runs the
install
Gradle task after stopping Gradle daemons.
- Uploading to a remote Maven repository
-
-
Set up a repository as explained in Setting Up a Private Artifact Repository.
-
Specify your repository and credentials for the project instead of the standard CUBA repository.
-
Open
build.gradle
of the component project in a text editor and adduploadRepository
to thecuba
section:cuba { //... // repository for uploading your artifacts uploadRepository { url = 'http://repo.company.com/nexus/content/repositories/snapshots' user = 'admin' password = 'admin123' } }
-
Open the component project in Studio.
-
In the Search dialog (Alt-/), find the
uploadArchives
Gradle task and run it. You can also run this task from the command line. The component’s artifacts will be uploaded to your repository. -
Remove the component artifacts from your local Maven repository to ensure that they will be downloaded from the remote repository during the next assembling of the application project: just delete the
.m2/repository/com/company
folder located in your user home directory. -
Now, when you assemble and run the application that uses this component, it will be downloaded from the remote repository.
-
- Uploading to Bintray
-
-
Register at https://bintray.com/signup/oss
TipYou can use social login (GitHub, Gmail, Twitter) on Bintray, but later you will have to reset your password, as this account’s password is required for getting an API-key (see below).
-
Get the Bintray user name. It can be found in the URL you see after login to Bintray. For example, in
https://bintray.com/vividphoenix
thevividphoenix
is the user name. -
Get the API-key. It can be found in Bintray interface if you edit your profile. In the API-key section, you will be asked to input your account password to obtain the key. Then you will be able to use this key and the username for plugin authentication:
-
The Bintray credentials can be added as environment variables:
BINTRAY_USER=your_bintray_user BINTRAY_API_KEY=9696c1cb90752357ded8fdf20eb3fa921bf9dbbb
-
Instead of environment variables, you can explicitly define these parameters in the
build.gradle
file of the project:bintray { user = 'bintray_user' key = 'bintray_api_key' ... }
-
Alternatively, you can provide Bintray credentials in the command line:
./gradlew clean assemble bintrayUpload -Pcuba.artifact.version=1.0.0 -PbintrayUser=your_bintray_user -PbintrayApiKey=9696c1cb90752357ded8fdf20eb3fa921bf9dbbb
-
-
Create a public repository of Maven type. Setting a license type is mandatory for open source (OSS) repositories.
Bintray structure implies using packages inside the repositories. At this stage, creating a package is not mandatory, as it will be automatically created while running the
gradle bintrayUpload
task. -
In your
build.gradle
, add the Bintray upload plugin dependency as follows:buildscript { // ... dependencies { classpath "com.haulmont.gradle:cuba-plugin:$cubaVersion" // Bintray upload plugin classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.0" } }
-
At the end of
build.gradle
, add the Bintray plugin settings:/** * If you have a multi-project build, make sure to apply the plugin and the plugin configuration to every project which artifacts you want to publish to Bintray. */ subprojects { apply plugin: 'com.jfrog.bintray' bintray { user = project.hasProperty('bintrayUser') ? project.property('bintrayUser') : System.getenv('BINTRAY_USER') key = project.hasProperty('bintrayApiKey') ? project.property('bintrayApiKey') : System.getenv('BINTRAY_API_KEY') configurations = ['archives'] // make files public ? publish = true // override existing artifacts? override = false // metadata pkg { repo = 'main' // your repository name name = 'amazingsearch' // package name - it will be created upon upload desc = 'AmasingSearch' // optional package description // organization name, if your repository is created inside an organization. // remove this parameter if you don't have an organization userOrg = 'jupiter-org' websiteUrl = 'https://github.com/jupiter/amazing-search' issueTrackerUrl = 'https://github.com/jupiter/amazing-search/issues' vcsUrl = 'https://github.com/jupiter/amazing-search.git' // mandatory for Open Source projects licenses = ["Apache-2.0"] labels = ['cuba-platform', 'opensource'] //githubRepo = 'amazingsearch/cuba-platform' // optional Github repository //githubReleaseNotesFile = 'README.md' // optional Github readme file } } }
-
here,
pkg:repo
is your repository (usemain
), -
pkg:name
is the package name (use your unique name, asamazingsearch
), -
pkg:desc
is the optional package description that will be shown on Bintray interface, -
and
pkg:userOrg
- is the name of an organization the repo belongs to (if not set, theBINTRAY_USER
will be used as the organization name by default).
-
-
Now you can build and upload the project with the following command:
./gradlew clean assemble bintrayUpload -Pcuba.artifact.version=1.0.0
-
If you publish the add-on on the CUBA Marketplace, its repository will be linked to the standard CUBA repositories and users won’t have to specify your repository in their projects.
-
4.6.3. Example of Application Component
In this section, we’ll consider a complete example of creating an application component and using it in a project. The component will provide a "Customer Management" functionality and include the Customer
entity and corresponding UI screens. The application will use the Customer
entity from the component as a reference in its Order
entity.
- Creating the Customer Management component
-
-
Create a new project in Studio and specify the following properties on the New project screen:
-
Project name -
customers
-
Project namespace -
cust
-
Root package -
com.company.customers
-
-
Edit Project properties and on the Advanced tab, set the Module prefix to
cust
. -
Create the
Customer
entity with at least thename
attribute. Switch to the Instance name tab and addname
to the name pattern attributes.WarningIf your component contains
@MappedSuperclass
persistent classes, make sure they have descendants which are entities (i.e. annotated with@Entity
) in the same project. Otherwise such base classes will not be properly enhanced and you will not be able to use them in applications. -
Generate DB scripts and create standard screens for the
Customer
entity:cust$Customer.browse
andcust$Customer.edit
. After that, go to main menu designer and rename theapplication
menu item tocustomerManagement
. -
Click to the App component descriptor link on the Project properties panel. Save the generated descriptor by clicking OK.
-
Test the Customer Management functionality: Run > Create database, Run > Start application server, then open
http://localhost:8080/cust
in your web browser. -
Install the application component into the local Maven repository by executing the Run > Install app component menu command.
-
- Creating the Sales application
-
-
Create a new project in Studio and specify the following properties on the New project screen:
-
Project name -
sales
-
Project namespace -
sales
-
Root package -
com.company.sales
-
-
Edit Project properties and on the Advanced tab check Use local Maven repository checkbox.
-
Switch back to the Main tab and on the App components panel click the plus button next to Custom components. In the Custom application component dialog, select the
customers
project in the Registered project drop-down list. The list contains all projects registered in Studio that have anapp-component.xml
descriptor. Click OK in the dialog. The Maven coordinates of the Customer Management component will appear in the list of custom components. Save the project properties by clicking OK. -
Create the
Order
entity and add thedate
andamount
attributes. Then add thecustomer
attribute as a many-to-one association with theCustomer
entity - it should be available in the Type drop-down list. -
Generate DB scripts and create standard screens for the
Order
entity. When creating standard screens, create anorder-with-customer
view that includes thecustomer
attribute and use it for the screens. -
Test the application functionality: Run > Create database, Run > Start application server, then open
http://localhost:8080/app
in your web browser. The application will contain two top level menu items: Customer Management and Application with the corresponding functionality.
-
- Modifying the Customer Management component
-
Suppose we have to change the component functionality (add an attribute to
Customer
) and then reassemble the application to incorporate the changes.-
Open the
customers
project in Studio. -
Edit the
Customer
entity and add theaddress
attribute. When saving the entity, select both browser and editor screens to include the new attribute. -
Generate DB scripts - a script for altering table will be created. Save the scripts.
-
Test the changes in the component: Run > Update database, Run > Start application server, then open
http://localhost:8080/cust
in your web browser. -
Re-install the application component into the local Maven repository by executing the Run > Install app component menu command.
-
Close the
sales
project in Studio (if it is open) and open it again. This is necessary to download the new component source code in Studio. -
Execute Build > Clean, then Build > Assemble project menu commands.
-
Execute Run > Update database - the update script from the Customer Management component will be executed.
-
Execute Run > Start application server and open
http://localhost:8080/app
in your web browser - the application will contain theCustomer
entity and screens with the newaddress
attribute.
-
4.6.4. Registering DispatcherServlet from Application Component
This section will learn you how to propagate the servlets and filters configuration from an application component to the owning application. To avoid the duplication of code in the web.xml file, you need to register your servlets and filters in the component using the special ServletRegistrationManager
bean.
The most common case of servlets registration is described through the example of HTTP servlet registration. Let’s consider a more complex example: an application component with a custom implementation of DispatcherServlet
for processing web requests.
This servlet loads its config from the demo-dispatcher-spring.xml
file, so to see it working you should create an empty file with such name in the root source directory (e.g. web/src
).
public class WebDispatcherServlet extends DispatcherServlet {
private volatile boolean initialized = false;
@Override
public String getContextConfigLocation() {
String configFile = "demo-dispatcher-spring.xml";
File baseDir = new File(AppContext.getProperty("cuba.confDir"));
String[] tokenArray = new StrTokenizer(configFile).getTokenArray();
StringBuilder locations = new StringBuilder();
for (String token : tokenArray) {
String location;
if (ResourceUtils.isUrl(token)) {
location = token;
} else {
if (token.startsWith("/"))
token = token.substring(1);
File file = new File(baseDir, token);
if (file.exists()) {
location = file.toURI().toString();
} else {
location = "classpath:" + token;
}
}
locations.append(location).append(" ");
}
return locations.toString();
}
@Override
protected WebApplicationContext initWebApplicationContext() {
WebApplicationContext wac = findWebApplicationContext();
if (wac == null) {
ApplicationContext parent = AppContext.getApplicationContext();
wac = createWebApplicationContext(parent);
}
onRefresh(wac);
String attrName = getServletContextAttributeName();
getServletContext().setAttribute(attrName, wac);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Published WebApplicationContext of servlet '" + getServletName() +
"' as ServletContext attribute with name [" + attrName + "]");
}
return wac;
}
@Override
public void init(ServletConfig config) throws ServletException {
if (!initialized) {
super.init(config);
initialized = true;
}
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
_service(response);
}
@Override
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
_service(res);
}
private void _service(ServletResponse res) throws IOException {
String testMessage = AppContext.getApplicationContext().getBean(Messages.class).getMainMessage("testMessage");
res.getWriter()
.write("WebDispatcherServlet test message: " + testMessage);
}
}
To register DispatcherServlet
, you have to load the class manually, instantiate it and initialize, otherwise different ClassLoaders may cause an issue in case of SingleWAR/SingleUberJAR deployment. Moreover, the custom DispatcherServlet
should be ready to double initialization - first time we initialize it manually, second time it is initialized by a servlet container.
Here is an example of a component that initializes WebDispatcherServlet
:
@Component
public class WebInitializer {
private static final String WEB_DISPATCHER_CLASS = "com.demo.comp.web.WebDispatcherServlet";
private static final String WEB_DISPATCHER_NAME = "web_dispatcher_servlet";
private final Logger log = LoggerFactory.getLogger(WebInitializer.class);
@Inject
private ServletRegistrationManager servletRegistrationManager;
@EventListener
public void initialize(ServletContextInitializedEvent e) {
Servlet webDispatcherServlet = servletRegistrationManager.createServlet(e.getApplicationContext(), WEB_DISPATCHER_CLASS);
ServletContext servletContext = e.getSource();
try {
webDispatcherServlet.init(new AbstractWebAppContextLoader.CubaServletConfig(WEB_DISPATCHER_NAME, servletContext));
} catch (ServletException ex) {
throw new RuntimeException("Failed to init WebDispatcherServlet");
}
servletContext.addServlet(WEB_DISPATCHER_NAME, webDispatcherServlet)
.addMapping("/webd/*");
}
}
The createServlet()
method of the injected ServletRegistrationManager
bean takes the application context from ServletContextInitializedEvent
and the fully-qualified name of the WebDispatcherServlet
class. In order to initialize the servlet, we pass the instance of ServletContext
obtained from ServletContextInitializedEvent
and the servlet name. The addMapping()
method is used to define an HTTP mapping for accessing the servlet via URL: /webd/
.
4.7. Miscellaneous
Here you can find various recipes that do not belong to the above categories.
4.7.1. Getting Localized Messages
This section covers ways of getting localized messages in different parts of the application.
-
In screen XML-descriptors, component attributes for displaying static text (such as caption) can address localized messages using the rules of MessageTools.loadString() method. For example:
-
caption="msg://roleName"
– gets a message defined by theroleName
key in the message pack of the current screen. Screen message pack is defined by themessagesPack
attribute of the rootwindow
element. -
caption="msg://com.company.sample.entity/Role.name"
– gets a message defined by theRole.name
key in thecom.company.sample.entity
message pack.
-
-
In screen controllers, localized strings can be retrieved in the following ways:
-
From the current screen message pack:
-
Using
getMessage()
method inherited from the AbstractFrame base class. For example:String msg = getMessage("warningMessage");
-
Using
formatMessage()
method inherited from theAbstractFrame
base class. In this case, the extracted message is used to format submitted parameters according to the rules ofString.format()
method. For example:messages.properties:
warningMessage = Invalid email address: '%s'
Java controller:
String msg = formatMessage("warningMessage", email);
-
-
From an arbitrary messages pack via an injection of Messages infrastructure interface. For example:
@Inject private Messages messages; @Override public void init(Map<String, Object> params) { String msg = messages.getMessage(getClass(), "warningMessage"); ... }
-
-
For components managed by a Spring container (managed beans, services, JMX-beans, Spring MVC controllers of the portal module), localized messages can be retrieved with the help of the Messages infrastructure interface injection:
@Inject protected Messages messages; ... String msg = messages.getMessage(getClass(), "warningMessage");
Localized messages for the Thymeleaf templates in the portal module can also be obtained by the message key from the main message pack of the portal module:
template<h1 th:text="#{messageKey}"></h1>
portal main message packmessageKey = Localized message
-
In application code where injection is not possible, the
Messages
interface can be obtained using the staticget()
method of theAppBeans
class:protected Messages messages = AppBeans.get(Messages.class); ... String msg = messages.getMessage(getClass(), "warningMessage");
4.7.2. Loading and Displaying Images
Let’s consider a task of loading, storing and displaying employee photos:
-
An employee is represented by
Employee
entity. -
Image files are stored in the FileStorage. The
Employee
entity contains a link to the correspondingFileDescriptor
. -
The
Employee
edit screen shows the picture and also supports uploading, downloading and clearing the picture.
Entity class with a link to the image file:
@Table(name = "SAMPLE_EMPLOYEE")
@Entity(name = "sample$Employee")
public class Employee extends StandardEntity {
...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "IMAGE_FILE_ID")
protected FileDescriptor imageFile;
public void setImageFile(FileDescriptor imageFile) {
this.imageFile = imageFile;
}
public FileDescriptor getImageFile() {
return imageFile;
}
}
A view for loading an Employee
together with FileDescriptor
should include all local attributes of FileDescriptor
:
<view class="com.company.sample.entity.Employee"
name="employee-edit">
<property name="name"/>
...
<property name="imageFile"
view="_local">
</property>
</view>
A fragment of the Employee
edit screen XML descriptor:
<groupBox caption="Photo" spacing="true"
height="300px" width="300px" expand="image">
<image id="image"
width="100%"
align="MIDDLE_CENTER"
scaleMode="CONTAIN"/>
<hbox align="BOTTOM_LEFT"
spacing="true">
<upload id="uploadField"/>
<button id="downloadImageBtn"
caption="Download"
invoke="onDownloadImageBtnClick"/>
<button id="clearImageBtn"
caption="Clear"
invoke="onClearImageBtnClick"/>
</hbox>
</groupBox>
Components used to display, upload and download images are contained within the groupBox container. Its top part shows a picture using the image component, while its bottom part from left to right contains the upload component and buttons to download and clear the image. As a result, this part of the screen should look like this:
Now, let us have a look at the edit screen controller.
import com.haulmont.cuba.core.entity.FileDescriptor;
import com.haulmont.cuba.core.global.FileStorageException;
import com.haulmont.cuba.gui.components.*;
import com.company.employeeimages.entity.Employee;
import com.haulmont.cuba.gui.data.DataSupplier;
import com.haulmont.cuba.gui.data.Datasource;
import com.haulmont.cuba.gui.export.ExportDisplay;
import com.haulmont.cuba.gui.export.ExportFormat;
import com.haulmont.cuba.gui.upload.FileUploadingAPI;
import javax.inject.Inject;
import java.util.Map;
public class EmployeeEdit extends AbstractEditor<Employee> {
@Inject
private DataSupplier dataSupplier;
@Inject
private FileUploadingAPI fileUploadingAPI;
@Inject
private ExportDisplay exportDisplay;
@Inject
private FileUploadField uploadField;
@Inject
private Button downloadImageBtn;
@Inject
private Button clearImageBtn;
@Inject
private Datasource<Employee> employeeDs;
@Inject
private Image image;
@Override
public void init(Map<String, Object> params) {
uploadField.addFileUploadSucceedListener(event -> {
FileDescriptor fd = uploadField.getFileDescriptor();
try {
fileUploadingAPI.putFileIntoStorage(uploadField.getFileId(), fd);
} catch (FileStorageException e) {
throw new RuntimeException("Error saving file to FileStorage", e);
}
getItem().setImageFile(dataSupplier.commit(fd));
displayImage();
});
uploadField.addFileUploadErrorListener(event ->
showNotification("File upload error", NotificationType.HUMANIZED));
employeeDs.addItemPropertyChangeListener(event -> {
if ("imageFile".equals(event.getProperty()))
updateImageButtons(event.getValue() != null);
});
}
@Override
protected void postInit() {
displayImage();
updateImageButtons(getItem().getImageFile() != null);
}
public void onDownloadImageBtnClick() {
if (getItem().getImageFile() != null)
exportDisplay.show(getItem().getImageFile(), ExportFormat.OCTET_STREAM);
}
public void onClearImageBtnClick() {
getItem().setImageFile(null);
displayImage();
}
private void updateImageButtons(boolean enable) {
downloadImageBtn.setEnabled(enable);
clearImageBtn.setEnabled(enable);
}
private void displayImage() {
if (getItem().getImageFile() != null) {
image.setSource(FileDescriptorResource.class).setFileDescriptor(getItem().getImageFile());
image.setVisible(true);
} else {
image.setVisible(false);
}
}
}
-
The
init()
method first initializes theuploadField
component that is used for uploading new images. In the case of a successful upload, a newFileDescriptor
instance is retrieved from the component and the corresponding files are sent from the temporary client storage toFileStorage
by invokingFileUploadingAPI.putFileIntoStorage()
. After that, theFileDescriptor
is saved to the database by invoking DataSupplier.commit(), and the saved instance is assigned to theimageFile
attribute of the editedEmployee
entity. Then, the controller’sdisplayImage()
method is invoked to display the uploaded image.After that, a listener is added in the
init()
method to the datasource containing anEmployee
instance. The listener enables or disables download and clear buttons, depending on the fact whether the file has been loaded or not. -
postInit()
method performs file display and refreshes the button states, depending on the existence of a loaded file. -
onDownloadImageBtnClick()
is invoked when thedownloadImageBtn
button is clicked; it downloads the file using the ExportDisplay interface. -
onClearImageBtnClick()
is invoked when theclearImageBtn
is clicked; it clears theimageFile
attribute of theEmployee
entity. The file is not deleted from storage. -
displayImage()
loads the file from storage and sets the content of theimage
component.
4.7.2.1. Displaying Images in a Table Column
To amplify the previous task, let’s add pictures to the table as employees' icons on the Employee
browse screen.
The pictures can be displayed in a separate column or inside any existing column. In both cases the Table.ColumnGenerator interface is used.
Below is a fragment of the Employee
browse screen XML descriptor:
<groupTable id="employeesTable"
width="100%">
<actions>
<action id="create"/>
<action id="edit"/>
<action id="remove"/>
</actions>
<columns>
<column id="name"/>
</columns>
<rows datasource="employeesDs"/>
<rowsCount/>
<buttonsPanel id="buttonsPanel"
alwaysVisible="true">
<button id="createBtn"
action="employeesTable.create"/>
<button id="editBtn"
action="employeesTable.edit"/>
<button id="removeBtn"
action="employeesTable.remove"/>
</buttonsPanel>
</groupTable>
To display pictures inline with an employee’s name in the name
column, let’s change the standard representation of data in this column. We will use the HBoxLayout container and place the Image component into it:
import com.haulmont.cuba.core.entity.FileDescriptor;
import com.haulmont.cuba.gui.components.*;
import com.company.employeeimages.entity.Employee;
import com.haulmont.cuba.gui.xml.layout.ComponentsFactory;
import javax.inject.Inject;
import java.util.Map;
import static com.haulmont.cuba.gui.components.Image.*;
public class EmployeeBrowse extends AbstractLookup {
@Inject
private ComponentsFactory componentsFactory;
@Inject
private GroupTable<Employee> employeesTable;
@Override
public void init(Map<String, Object> params) {
employeesTable.addGeneratedColumn("name", entity -> {
Image image = componentsFactory.createComponent(Image.class);
image.setScaleMode(ScaleMode.CONTAIN);
image.setHeight("40");
image.setWidth("40");
FileDescriptor userImageFile = entity.getImageFile();
image.setSource(FileDescriptorResource.class).setFileDescriptor(userImageFile);
Label userLogin = componentsFactory.createComponent(Label.class);
userLogin.setValue(entity.getName());
userLogin.setAlignment(Alignment.MIDDLE_LEFT);
HBoxLayout hBox = componentsFactory.createComponent(HBoxLayout.class);
hBox.setSpacing(true);
hBox.add(image);
hBox.add(userLogin);
return hBox;
});
}
}
-
The
init()
method invokes theaddGeneratedColumn()
method that takes two parameters: an identifier of the column and an implementation of theTable.ColumnGenerator
interface. The latter is used to define the custom representation of data in thename
column. -
Inside this method we create an
Image
component using theComponentsFactory
interface. We set the scale mode of the component (CONTAIN
) and its size parameters. -
Then we get the
FileDescriptor
instance with the picture stored in the File Storage. The link to this picture is stored in theimageFile
attribute of theEmployee
entity. TheFileDescriptorImageResource
resource type is used to set the source for theImage
component. -
We will display the
name
attribute in theLabel
component alongside the picture. -
We will wrap both
Image
andLabel
components into theHBoxLayout
container, and make theaddGeneratedColumn()
method return this container as the new table cell layout.
You can also use a more declarative approach with the generator XML attribute.
4.7.3. Sending Emails
This section contains a practical guide to sending emails using the CUBA email sending mechanism.
Let us consider the following task:
-
There are the
NewsItem
entity and theNewsItemEdit
screen. -
The
NewsItem
entity contains the following attributes:date
,caption
,content
. -
We want to send emails to some addresses every time a new instance of
NewsItem
is created through theNewsItemEdit
screen. An email should containNewsItem.caption
as a subject and the message body should be created from a template includingNewsItem.content
.
-
Add the following code to
NewsItemEdit.java
:public class NewsItemEdit extends AbstractEditor<NewsItem> { // Indicates that a new item was created in this editor private boolean justCreated; @Inject protected EmailService emailService; // This method is invoked when a new item is initialized @Override protected void initNewItem(NewsItem item) { justCreated = true; } // This method is invoked after the screen commit @Override protected boolean postCommit(boolean committed, boolean close) { if (committed && justCreated) { // If a new entity was saved to the database, ask a user about sending an email showOptionDialog( "Email", "Send the news item by email?", MessageType.CONFIRMATION, new Action[] { new DialogAction(DialogAction.Type.YES) { @Override public void actionPerform(Component component) { sendByEmail(); } }, new DialogAction(DialogAction.Type.NO) } ); } return super.postCommit(committed, close); } // Queues an email for sending asynchronously private void sendByEmail() { NewsItem newsItem = getItem(); EmailInfo emailInfo = new EmailInfo( "john.doe@company.com,jane.roe@company.com", // recipients newsItem.getCaption(), // subject null, // the "from" address will be taken from the "cuba.email.fromAddress" app property "com/company/demo/templates/news_item.txt", // body template Collections.singletonMap("newsItem", newsItem) // template parameters ); emailService.sendEmailAsync(emailInfo); } }
As you can see, the
sendByEmail()
method invokes theEmailService
and passes theEmailInfo
instance describing the the messages. The body of the messages will be created on the basis of thenews_item.txt
template. -
Create the body template file
news_item.txt
in thecom.company.demo.templates
package of the core module:The company news: ${newsItem.content}
This is a Freemarker template which will use parameters passed in the
EmailInfo
instance (newsItem
in this case). -
Launch the application, open the
NewsItem
entity browser and click Create. The editor screen will be opened. Fill in the fields and press OK. The confirmation dialog with the question about sending emails will be shown. Click Yes. -
Go to the Administration > Email History screen of your application. You will see two records (by the number of recipients) with the
Queue
status. It means that the emails are in the queue and not yet sent. -
To process the queue, set up a scheduled task. Go to the Administration > Scheduled Tasks screen of your application. Create a new task and set the following parameters:
-
Bean Name -
cuba_Emailer
-
Method Name -
processQueuedEmails()
-
Singleton - yes (this is important only for a cluster of middleware servers)
-
Period, sec - 10
Save the task and click Activate on it.
If you did not set up the scheduled tasks execution for this project before, nothing will happen on this stage - the task will not be executed until you start the whole scheduling mechanism.
-
-
Open the
modules/core/src/app.properties
file and add the following property:cuba.schedulingActive = true
Restart the application server. The scheduling mechanism is now active and invokes the email queue processing.
-
Go to the Administration > Email History screen. The status of the emails will be
Sent
if they were successfully sent, or, most probably,Sending
orQueue
otherwise. In the latter case, you can open the application log inbuild/tomcat/logs/app.log
and find out the reason. The email sending mechanism will take several (10 by default) attempts to send the messages and if they fail, set the status toNot sent
. -
The most obvious reason that emails cannot be sent is that you have not set up the SMTP server parameters. You can set the parameters in the database through the
app-core.cuba:type=Emailer
JMX bean or in the application properties file of your middleware. Let us consider the latter. Open themodules/core/src/app.properties
file and add the required parameters:cuba.email.fromAddress = do-not-reply@company.com cuba.email.smtpHost = mail.company.com
Restart the application server. Go to Administration > JMX Console, find the
Emailer
JMX bean and try to send a test email to yourself using thesendTestEmail()
operation. -
Now your sending mechanism is set up correctly, but it will not send the messages in the
Not sent
state. So you have to create anotherNewsItem
in the editor screen. Do it and then watch how the status of new messages in the Email History screen will change toSent
.
4.7.4. Creating Custom Visual Components
As explained in the Custom Visual Components section, the standard set of visual components can be extended in your project. You have the following options:
-
Integrate a Vaadin add-on. Many third-party Vaadin components are distributed as add-ons and available at
https://vaadin.com/directory
. -
Integrate a JavaScript component. You can create a Vaadin component using a JavaScript library.
-
Create a new Vaadin component with the client part written on GWT.
Futher on, you can integrate the resulting Vaadin component into CUBA Generic UI to be able to use it declaratively in screen XML descriptors and bind to datasources.
And the final step of integration is the support of the new component in the Studio WYSIWYG layout editor.
This section gives you examples of creating new visual components with all the methods described above. Integration to the Generic UI and support in Studio are the same for all methods, so these topics are described only for a new component created on the basis of a Vaadin add-on.
4.7.4.1. Using a Third-party Vaadin Component
This is an example of using the Stepper component available at http://vaadin.com/addon/stepper
, in an application project. The component enables changing text field value in steps using the keyboard, mouse scroll or built-in up/down buttons.
Create a new project in CUBA Studio and name it addon-demo
.
A Vaadin add-on may be integrated if the application project has a web-toolkit module. Create the module by clicking the Create web toolkit module link of the Project properties navigator section.
Then click the New UI component link. The UI component generation page will open. Select the Vaadin add-on value in the Component type section.
Fill in the following fields:
-
The Add-on Maven dependency field contains Maven coordinates of the Vaadin add-on. The add-on will be included as a dependency to the project. You can define coordinates in two formats:
-
As an XML copied from the add-on web site (http://vaadin.com/addon/stepper):
<dependency> <groupId>org.vaadin.addons</groupId> <artifactId>stepper</artifactId> <version>2.2.2</version> </dependency>
-
In one line as you add dependencies in build.gradle:
org.vaadin.addons:stepper:2.2.2
-
-
The Inherited widgetset field contains a widgetset name of the add-on:
org.vaadin.risto.stepper.widgetset.StepperWidgetset
-
Integrate into generic UI - deselect this checkbox as we do not integrate the component into the Generic UI in this example.
Press the OK button.
If you open the project in the IDE, you can see that Studio has changed two files:
-
In
build.gradle
, the web module now contains a dependency on the add-on that contains the component.configure(webModule) { ... dependencies { ... compile("org.vaadin.addons:stepper:2.2.2") }
-
The
AppWidgetSet.gwt.xml
file of the web-toolkit module now inherits the add-on widgetset:<module> <inherits name="com.haulmont.cuba.web.toolkit.ui.WidgetSet" /> <inherits name="org.vaadin.risto.stepper.widgetset.StepperWidgetset" /> <set-property name="user.agent" value="safari" /> </module>
TipYou can speed up the widgetset compilation by defining the
user.agent
property. In this example, widgetset will be compiled only for browsers based on WebKit: Chrome, Safari, etc.
Now the component from the Vaadin add-on is included to the project. Let’s see how to use it in the project screens.
-
Create a new entity
Customer
with two fields:-
name
of type String -
score
of type Integer
-
-
Generate standard screens for the new entity. Ensure that the In module field is set to
Web Module
. Screens that use Vaadin components directly must be placed in the web module.TipActually, screens can be placed in the gui module as well, but then the code that uses the Vaadin component should be moved to a separate companion.
-
Next, we will add the
stepper
component to the screen. You can place it in a FieldGroup or in a separate container. Let’s examine both methods.-
Add the
custom = "true"
attribute to thescore
field of thefieldGroup
component of thecustomer-edit.xml
screen.<?xml version="1.0" encoding="UTF-8" standalone="no"?> <window xmlns="http://schemas.haulmont.com/cuba/window.xsd" caption="msg://editCaption" class="com.company.addondemo.web.customer.CustomerEdit" datasource="customerDs" focusComponent="fieldGroup" messagesPack="com.company.addondemo.web.customer"> <dsContext> <datasource id="customerDs" class="com.company.addondemo.entity.Customer" view="_local"/> </dsContext> <layout expand="windowActions" spacing="true"> <fieldGroup id="fieldGroup" datasource="customerDs"> <column width="250px"> <field property="name"/> <field property="score" custom="true"/> </column> </fieldGroup> <frame id="windowActions" screen="editWindowActions"/> </layout> </window>
Add the following code to the
CustomerEdit.java
controller:package com.company.addondemo.web.customer; import com.haulmont.cuba.gui.components.AbstractEditor; import com.company.addondemo.entity.Customer; import com.haulmont.cuba.gui.components.Component; import com.haulmont.cuba.gui.components.FieldGroup; import com.haulmont.cuba.gui.components.VBoxLayout; import com.haulmont.cuba.gui.data.Datasource; import com.haulmont.cuba.gui.xml.layout.ComponentsFactory; import com.haulmont.cuba.web.gui.components.WebComponentsHelper; import com.vaadin.ui.Layout; import org.vaadin.risto.stepper.IntStepper; import javax.inject.Inject; import java.util.Map; public class CustomerEdit extends AbstractEditor<Customer> { @Inject private ComponentsFactory componentsFactory; @Inject private FieldGroup fieldGroup; @Inject private Datasource<Customer> customerDs; private IntStepper stepper = new IntStepper(); @Override public void init(Map<String, Object> params) { fieldGroup.createField("score"); Component box = componentsFactory.createComponent(VBoxLayout.class); fieldGroup.getFieldNN("score").setComponent(box); Layout layout = (Layout) WebComponentsHelper.unwrap(box); layout.addComponent(stepper); stepper.setSizeFull(); stepper.addValueChangeListener(event -> customerDs.getItem().setValue("score", event.getProperty().getValue()) ); } @Override protected void initNewItem(Customer item) { item.setScore(0); } @Override protected void postInit() { stepper.setValue(getItem().getScore()); } }
The
init()
method initializes the customscore
field. TheComponentsFactory
creates an instance of BoxLayout, retrieves a link to the Vaadin container via WebComponentsHelper, and adds the new component to it. TheBoxLayout
is then returned to be used in the custom field.Data binding is implemented programmatically by setting a current value to the
stepper
component from the editedCustomer
instance in thepostInit()
method. Additionally, the corresponding entity attribute is updated through the value change listener, when the user changes the value. -
The new component can be used in any part of the screen outside of the
FieldGroup
. In order to do this, declare thescoreBox
container in the XML-descriptor:<?xml version="1.0" encoding="UTF-8" standalone="no"?> <window xmlns="http://schemas.haulmont.com/cuba/window.xsd" caption="msg://editCaption" class="com.company.addondemo.web.customer.CustomerEdit" datasource="customerDs" focusComponent="fieldGroup" messagesPack="com.company.addondemo.web.customer"> <dsContext> <datasource id="customerDs" class="com.company.addondemo.entity.Customer" view="_local"/> </dsContext> <layout expand="windowActions" spacing="true"> <fieldGroup id="fieldGroup" datasource="customerDs"> <column width="250px"> <field property="name"/> </column> </fieldGroup> <hbox id="scoreBox" spacing="true"> <label value="Score" align="MIDDLE_LEFT"/> </hbox> <frame id="windowActions" screen="editWindowActions"/> </layout> </window>
Inject the container to the screen controller, retrieve a link to the underlying Vaadin container and add the component to it:
package com.company.addondemo.web.customer; import com.haulmont.cuba.gui.components.*; import com.company.addondemo.entity.Customer; import com.haulmont.cuba.gui.data.Datasource; import com.haulmont.cuba.web.gui.components.WebComponentsHelper; import com.vaadin.ui.Layout; import org.vaadin.risto.stepper.IntStepper; import javax.inject.Inject; import java.util.Map; public class CustomerEdit extends AbstractEditor<Customer> { @Inject private FieldGroup fieldGroup; @Inject private Datasource<Customer> customerDs; @Inject private BoxLayout scoreBox; private IntStepper stepper = new IntStepper(); @Override public void init(Map<String, Object> params) { Layout box = (Layout) WebComponentsHelper.unwrap(scoreBox); box.addComponent(stepper); stepper.setWidth("250px"); fieldGroup.addField(fieldGroup.createField("score")); stepper.addValueChangeListener(event -> customerDs.getItem().setValue("score", event.getProperty().getValue()) ); } @Override protected void initNewItem(Customer item) { item.setScore(0); } @Override protected void postInit() { stepper.setValue(getItem().getScore()); } }
Data binding is implemented in the same way as described above.
-
-
To adapt the component style, create a theme extension in the project. Click the Create theme extension link in the Project properties navigator section. Select the
halo
theme. After that, open thethemes/halo/com.company.application/halo-ext.scss
file located in the web module and add the following code:/* Define your theme modifications inside next mixin */ @mixin com_company_application-halo-ext { @include halo; /* Basic styles for stepper inner text box */ .stepper input[type="text"] { @include box-defaults; @include valo-textfield-style; &:focus { @include valo-textfield-focus-style; } } }
-
Start the application server. The resulting editor screen will look as follows:
4.7.4.2. Integrating a Vaadin Component into the Generic UI
In the previous section, we have included the third-party Stepper component in the project. In this section, we will integrate it into CUBA Generic UI. This will allow developers to use the component declaratively in the screen XML and bind it to the data model entities through datasources.
Create a new project in CUBA Studio and name it addon-gui-demo
.
Create the web-toolkit module by clicking the Create web toolkit module link of the Project properties navigator section.
Then click the New UI component link. The UI component generation page will open. Select the Vaadin add-on value in the Component type section.
Fill in the Add-on Maven dependency and Inherited widgetset as described in the previous section.
Then fill in the fields of the bottom section:
-
Integrate into Generic UI - defines that a component should be integrated into the Generic UI.
-
Component XML element - an element to be used in screen XML descriptors. Enter
stepper
. -
Component interface name - a name of the component Generic UI interface. Enter
Stepper
. -
FQN of the Vaadin component from add-on - fully qualified class name of the Vaadin component from the add-on. In our case it is
org.vaadin.risto.stepper.IntStepper
.
When you click OK, Studio will do the following:
-
Add the Vaadin add-on as a web module dependency in
build.gradle
. -
Include add-on widgetset in
AppWidgetSet.gwt.xml
of web-toolkit module. -
Generate stubs for the following files:
-
Stepper
- an interface of the component in the gui subfolder of the web module. -
WebStepper
- a component implementation in the gui subfolder of the web module. -
StepperLoader
- a component XML-loader in the web module. -
ui-component.xsd
- a new component XML schema definition. If the file already exists, the information about the new component will be added to the existing file. -
cuba-ui-component.xml
- the file that registers a new component loader in web module. If the file already exists, the information about the new component will be added to the existing file.
-
Open the project in the IDE.
Let’s walk through generated files add make necessary changes.
-
Open the
Stepper
interface in the gui subfolder of the web module. Replace its content with the following code:package com.company.addonguidemo.web.gui.components; import com.haulmont.cuba.gui.components.Field; // note that Stepper should extend Field public interface Stepper extends Field { String NAME = "stepper"; boolean isManualInputAllowed(); void setManualInputAllowed(boolean value); boolean isMouseWheelEnabled(); void setMouseWheelEnabled(boolean value); int getStepAmount(); void setStepAmount(int amount); int getMaxValue(); void setMaxValue(int maxValue); int getMinValue(); void setMinValue(int minValue); }
The base interface for the component is
Field
, which is designed to display and edit an entity attribute. -
Open the
WebStepper
class - a component implementation in the gui subfolder of the web module. Replace its content with the following code:package com.company.addonguidemo.web.gui.components; import com.company.addonguidemo.web.gui.components.Stepper; import com.haulmont.cuba.web.gui.components.WebAbstractField; import org.vaadin.risto.stepper.IntStepper; // note that WebStepper should extend WebAbstractField public class WebStepper extends WebAbstractField<IntStepper> implements Stepper { public WebStepper() { this.component = new org.vaadin.risto.stepper.IntStepper(); } @Override public boolean isManualInputAllowed() { return component.isManualInputAllowed(); } @Override public void setManualInputAllowed(boolean value) { component.setManualInputAllowed(value); } @Override public boolean isMouseWheelEnabled() { return component.isMouseWheelEnabled(); } @Override public void setMouseWheelEnabled(boolean value) { component.setMouseWheelEnabled(value); } @Override public int getStepAmount() { return component.getStepAmount(); } @Override public void setStepAmount(int amount) { component.setStepAmount(amount); } @Override public int getMaxValue() { return component.getMaxValue(); } @Override public void setMaxValue(int maxValue) { component.setMaxValue(maxValue); } @Override public int getMinValue() { return component.getMinValue(); } @Override public void setMinValue(int minValue) { component.setMinValue(minValue); } }
The chosen base class is
WebAbstractField
, which implements the methods of theField
interface. -
The
StepperLoader
class in web module loads the component from its representation in XML.package com.company.addonguidemo.web.gui.xml.layout.loaders; import com.company.addonguidemo.web.gui.components.Stepper; import com.haulmont.cuba.gui.xml.layout.loaders.AbstractFieldLoader; public class StepperLoader extends AbstractFieldLoader<Stepper> { @Override public void createComponent() { resultComponent = factory.createComponent(Stepper.class); loadId(resultComponent, element); } @Override public void loadComponent() { super.loadComponent(); String manualInput = element.attributeValue("manualInput"); if (manualInput != null) { resultComponent.setManualInputAllowed(Boolean.parseBoolean(manualInput)); } String mouseWheel = element.attributeValue("mouseWheel"); if (mouseWheel != null) { resultComponent.setMouseWheelEnabled(Boolean.parseBoolean(mouseWheel)); } String stepAmount = element.attributeValue("stepAmount"); if (stepAmount != null) { resultComponent.setStepAmount(Integer.parseInt(stepAmount)); } String maxValue = element.attributeValue("maxValue"); if (maxValue != null) { resultComponent.setMaxValue(Integer.parseInt(maxValue)); } String minValue = element.attributeValue("minValue"); if (minValue != null) { resultComponent.setMinValue(Integer.parseInt(minValue)); } } }
The
AbstractFieldLoader
class contains code for loading basic properties of theField
component. SoStepperLoader
loads only the specific properties of theStepper
component. -
The
cuba-ui-component.xml
file in the web module registers the new component and its loader. Leave the file unchanged.<?xml version="1.0" encoding="UTF-8" standalone="no"?> <components xmlns="http://schemas.haulmont.com/cuba/components.xsd"> <component> <name>stepper</name> <componentLoader>com.company.addonguidemo.web.gui.xml.layout.loaders.StepperLoader</componentLoader> <class>com.company.addonguidemo.web.gui.components.WebStepper</class> </component> </components>
-
The
ui-component.xsd
file in web module contains XML schema definitions of custom visual components. Add thestepper
attributes definition.<?xml version="1.0" encoding="UTF-8" standalone="no"?> <xs:schema xmlns="http://schemas.company.com/agd/0.1/ui-component.xsd" attributeFormDefault="unqualified" elementFormDefault="qualified" targetNamespace="http://schemas.company.com/agd/0.1/ui-component.xsd" xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:element name="stepper"> <xs:complexType> <xs:attribute name="id" type="xs:string"/> <xs:attribute name="caption" type="xs:string"/> <xs:attribute name="width" type="xs:string"/> <xs:attribute name="height" type="xs:string"/> <xs:attribute name="datasource" type="xs:string"/> <xs:attribute name="property" type="xs:string"/> <xs:attribute name="manualInput" type="xs:boolean"/> <xs:attribute name="mouseWheel" type="xs:boolean"/> <xs:attribute name="stepAmount" type="xs:int"/> <xs:attribute name="maxValue" type="xs:int"/> <xs:attribute name="minValue" type="xs:int"/> </xs:complexType> </xs:element> </xs:schema>
Let’s see how to add the new component to a screen.
-
Create a new entity
Customer
. The entity have two fields:-
name
of type String -
score
of type Integer
-
-
Generate standard screens for the new entity.
-
Add the
stepper
component to the editor screen. You can place it in a FieldGroup or in a separate container. We’ll examine both methods.-
Using the component inside a container.
-
Open the
customer-edit.xml
file. -
Define the new namespace
xmlns:app="http://schemas.company.com/agd/0.1/ui-component.xsd"
. -
Remove the
score
field fromfieldGroup
. -
Add
stepper
component to the screen.
As a result, the XML descriptor should look like this:
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <window xmlns="http://schemas.haulmont.com/cuba/window.xsd" xmlns:app="http://schemas.company.com/agd/0.1/ui-component.xsd" caption="msg://editCaption" class="com.company.addonguidemo.web.customer.CustomerEdit" datasource="customerDs" focusComponent="fieldGroup" messagesPack="com.company.addonguidemo.web.customer"> <dsContext> <datasource id="customerDs" class="com.company.addonguidemo.entity.Customer" view="_local"/> </dsContext> <layout expand="windowActions" spacing="true"> <fieldGroup id="fieldGroup" datasource="customerDs"> <column width="250px"> <field property="name"/> </column> </fieldGroup> <app:stepper id="stepper" datasource="customerDs" property="score" caption="Score" minValue="1" maxValue="20"/> <frame id="windowActions" screen="editWindowActions"/> </layout> </window>
In the example above, the
stepper
component is associated with thescore
attribute of theCustomer
entity. An instance of this entity is managed by thecustomerDs
datasource. -
-
Using the new component inside a FieldGroup:
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <window xmlns="http://schemas.haulmont.com/cuba/window.xsd" caption="msg://editCaption" class="com.company.addonguidemo.web.customer.CustomerEdit" datasource="customerDs" focusComponent="fieldGroup" messagesPack="com.company.addonguidemo.web.customer"> <dsContext> <datasource id="customerDs" class="com.company.addonguidemo.entity.Customer" view="_local"/> </dsContext> <layout expand="windowActions" spacing="true"> <fieldGroup id="fieldGroup" datasource="customerDs"> <column width="250px"> <field property="name"/> <field property="score" custom="true"/> </column> </fieldGroup> <frame id="windowActions" screen="editWindowActions"/> </layout> </window>
package com.company.addonguidemo.web.customer; import com.company.addonguidemo.gui.components.Stepper; import com.haulmont.cuba.gui.components.AbstractEditor; import com.company.addonguidemo.entity.Customer; import com.haulmont.cuba.gui.components.FieldGroup; import com.haulmont.cuba.gui.data.Datasource; import com.haulmont.cuba.gui.xml.layout.ComponentsFactory; import javax.inject.Inject; import java.util.Map; public class CustomerEdit extends AbstractEditor<Customer> { @Inject private ComponentsFactory componentsFactory; @Inject private FieldGroup fieldGroup; @Inject private Datasource<Customer> customerDs; @Override public void init(Map<String, Object> params) { Stepper stepper = componentsFactory.createComponent(Stepper.class); stepper.setDatasource(customerDs, "score"); stepper.setWidth("100%"); fieldGroup.getFieldNN("score").setComponent(stepper); } }
-
-
To adapt the component style, create a theme extension in the project. Click the Create theme extension link in the Project properties navigator section. Select the
halo
theme. After that, open thethemes/halo/com.company.application/halo-ext.scss
file located in the web module and add the following code:/* Define your theme modifications inside next mixin */ @mixin com_company_application-halo-ext { /* Basic styles for stepper inner text box */ .stepper input[type="text"] { @include box-defaults; @include valo-textfield-style; &:focus { @include valo-textfield-focus-style; } } }
-
Start the application server. The resulting editor screen will look as follows:
4.7.4.3. Using a JavaScript library
In this example, we will use the Slider component from the jQuery UI library. The slider will have two drag handlers that define a values range.
Create a new project in CUBA Studio and name it jscomponent
.
Click the New UI component button on the Project properties navigator section. The UI component generation will open. Select the JavaScript component
value in the Component type
section.
Enter SliderServerComponent
in the Vaadin component class name field.
Deselect the Integrate into Generic UI flag. The process of integration into the Generic UI is the same as described at Integrating a Vaadin Component into the Generic UI, so we won’t repeat it here.
After clicking the OK button Studio will generate the following files:
-
SliderServerComponent
- a Vaadin component integrated with JavaScript. -
SliderState
- a state class of the Vaadin component. -
slider-connector.js
- a JavaScript connector for the Vaadin component.
Let’s examine the generated files and make necessary changes in the source code.
-
SlideState
state class defines what data is transferred between the server and the client. In our case it is a minimal possible value, maximum possible value and selected values.package com.company.jscomponent.web.toolkit.ui.slider; import com.vaadin.shared.ui.JavaScriptComponentState; public class SliderState extends JavaScriptComponentState { public double[] values; public double minValue; public double maxValue; }
-
Vaadin server-side component
SliderServerComponent
.package com.company.jscomponent.web.toolkit.ui.slider; import com.vaadin.annotations.StyleSheet; import com.vaadin.ui.AbstractJavaScriptComponent; import com.vaadin.annotations.JavaScript; import elemental.json.JsonArray; @JavaScript({"slider-connector.js", "jquery-ui.js"}) @StyleSheet({"jquery-ui.css"}) public class SliderServerComponent extends AbstractJavaScriptComponent { public interface ValueChangeListener { void valueChanged(double[] newValue); } private ValueChangeListener listener; public SliderServerComponent() { addFunction("valueChanged", arguments -> { JsonArray array = arguments.getArray(0); double[] values = new double[2]; values[0] = array.getNumber(0); values[1] = array.getNumber(1); getState(false).values = values; listener.valueChanged(values); }); } public void setValue(double[] value) { getState().values = value; } public double[] getValue() { return getState().values; } public double getMinValue() { return getState().minValue; } public void setMinValue(double minValue) { getState().minValue = minValue; } public double getMaxValue() { return getState().maxValue; } public void setMaxValue(double maxValue) { getState().maxValue = maxValue; } @Override protected SliderState getState() { return (SliderState) super.getState(); } @Override public SliderState getState(boolean markAsDirty) { return (SliderState) super.getState(markAsDirty); } public ValueChangeListener getListener() { return listener; } public void setListener(ValueChangeListener listener) { this.listener = listener; } }
The server component defines getters and setters to work with the slider state and an interface of value change listeners. The class extends
AbstractJavaScriptComponent
.The
addFunction()
method invocation in the class constructor defines a handler for an RPC-call of thevalueChanged()
function from the client.The
@JavaScript
and@StyleSheet
annotations point to files that must be loaded on the web page. In our example, these are JavaScript files of the jquery-ui library, the connector and the stylesheet for jquery-ui. You should place these files to the Java package of the Vaadin server component.
Download an archive with jQuery UI from http://jqueryui.com/download and put files jquery-ui.js
and jquery-ui.css
from the archive to the Java package of the SliderServerComponent
class. At the jQuery UI download page, you can select which components should be put into the archive. For this demo, it is enough to select only the Slider
item of the Widgets
group.
-
JavaScript connector
slider-connector.js
.com_company_jscomponent_web_toolkit_ui_slider_SliderServerComponent = function() { var connector = this; var element = connector.getElement(); $(element).html("<div/>"); $(element).css("padding", "5px 10px"); var slider = $("div", element).slider({ range: true, slide: function(event, ui) { connector.valueChanged(ui.values); } }); connector.onStateChange = function() { var state = connector.getState(); slider.slider("values", state.values); slider.slider("option", "min", state.minValue); slider.slider("option", "max", state.maxValue); $(element).width(state.width); } }
Connector is a function that initializes a JavaScript component when the web page is loaded. The function name must correspond to the server component class name where dots in package name are replaced with underscore characters.
Vaadin adds several useful methods to the connector function.
this.getElement()
returns an HTML DOM element of the component,this.getState()
returns a state object.Our connector does the following:
-
Initializes the
slider
component of the jQuery UI library. Theslide()
function is invoked when the position of any drag handler changes. This function in turn invokes thevalueChanged()
connector method.valuedChanged()
is the method that we defined on the server side in theSliderServerComponent
class. -
Defines the
onStateChange()
function. It is called when the state object is changed on the server side.
-
To demonstrate how the component works, let’s create the Product
entity with three attributes:
-
name
of type String -
minDiscount
of type Double -
maxDiscount
of type Double
Generate standard screens for the entity. Ensure that the value of the In module field is Web Module
.
The slider
component will set minimal and maximum discount values of a product.
Open the product-edit.xml
file. Make minDiscount
and maxDiscount
fields not editable by adding the editable="false"
attribute to the corresponding elements. Then add the new custom slider
field to the fieldGroup
.
As a result, the XML descriptor of the editor screen should look as follows:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
caption="msg://editCaption"
class="com.company.jscomponent.web.product.ProductEdit"
datasource="productDs"
focusComponent="fieldGroup"
messagesPack="com.company.jscomponent.web.product">
<dsContext>
<datasource id="productDs"
class="com.company.jscomponent.entity.Product"
view="_local"/>
</dsContext>
<layout expand="windowActions"
spacing="true">
<fieldGroup id="fieldGroup"
datasource="productDs">
<column width="250px">
<field property="name"/>
<field property="minDiscount" editable="false"/>
<field property="maxDiscount" editable="false"/>
<field id="slider" custom="true"/>
</column>
</fieldGroup>
<frame id="windowActions"
screen="editWindowActions"/>
</layout>
</window>
Open the ProductEit.java
file. Replace its content with the following code:
package com.company.jscomponent.web.product;
import com.company.jscomponent.web.toolkit.ui.slider.SliderServerComponent;
import com.haulmont.cuba.gui.components.AbstractEditor;
import com.company.jscomponent.entity.Product;
import com.haulmont.cuba.gui.components.Component;
import com.haulmont.cuba.gui.components.FieldGroup;
import com.haulmont.cuba.gui.components.VBoxLayout;
import com.haulmont.cuba.gui.data.Datasource;
import com.haulmont.cuba.gui.xml.layout.ComponentsFactory;
import com.haulmont.cuba.web.gui.components.WebComponentsHelper;
import com.vaadin.ui.Layout;
import javax.inject.Inject;
public class ProductEdit extends AbstractEditor<Product> {
@Inject
private FieldGroup fieldGroup;
@Inject
private ComponentsFactory componentsFactory;
@Inject
private Datasource<Product> productDs;
@Override
protected void initNewItem(Product item) {
super.initNewItem(item);
item.setMinDiscount(15.0);
item.setMaxDiscount(70.0);
}
@Override
protected void postInit() {
super.postInit();
Component box = componentsFactory.createComponent(VBoxLayout.class);
Layout vBox = (Layout) WebComponentsHelper.unwrap(box);
SliderServerComponent slider = new SliderServerComponent();
slider.setValue(new double[]{getItem().getMinDiscount(), getItem().getMaxDiscount()});
slider.setMinValue(0);
slider.setMaxValue(100);
slider.setWidth("240px");
slider.setListener(newValue -> {
getItem().setMinDiscount(newValue[0]);
getItem().setMaxDiscount(newValue[1]);
});
vBox.addComponent(slider);
fieldGroup.getFieldNN("slider").setComponent(box);
}
}
The initNewItem()
method sets initial values for discounts of a new product.
Method init()
initializes the slider
custom field. It sets current, minimal and maximum values of the slider
and defines the value change listener. When the drag handler moves, a new value will be set to the corresponding field of the editable entity.
Start the application server and open the product editor screen. Changing the drop handler position must change the value of the text fields.
4.7.4.4. Creating a GWT component
In this section, we will cover the creation of a simple GWT component (a rating field consisting of 5 stars) and its usage in application screens.
Create a new project in CUBA Studio and name it ratingsample
.
Click the Create web-toolkit module link in the Project properties navigator section.
Click the New UI component link. The UI component generation page will open. Select the New GWT component value in the Component type section.
Enter RatingFieldServerComponent
in the Vaadin component class name field.
Deselect the Integrate into Generic UI flag. The process of integration into the Generic UI is the same as described at Integrating a Vaadin Component into the Generic UI, so we won’t repeat it here.
After clicking the OK button Studio generates the following files:
-
RatingFieldWidget.java
- a GWT widget in web-toolkit module. -
RatingFieldServerComponent.java
- a Vaadin component class. -
RatingFieldState.java
- a component state class. -
RatingFieldConnector.java
- a connector that links the client code with the server component. -
RatingFieldServerRpc.java
- a class that defines a server API for the client.
Let’s look at the generated files and make necessary changes in them.
-
RatingFieldWidget
is a GWT widget. Replace its content with the following code:package com.company.ratingsample.web.toolkit.ui.client.ratingfield; import com.google.gwt.dom.client.DivElement; import com.google.gwt.dom.client.SpanElement; import com.google.gwt.dom.client.Style.Display; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.ui.FocusWidget; import java.util.ArrayList; import java.util.List; public class RatingFieldWidget extends FocusWidget { private static final String CLASSNAME = "ratingfield"; // API for handle clicks public interface StarClickListener { void starClicked(int value); } protected List<SpanElement> stars = new ArrayList<SpanElement>(5); protected StarClickListener listener; protected int value = 0; public RatingFieldWidget() { DivElement container = DOM.createDiv().cast(); container.getStyle().setDisplay(Display.INLINE_BLOCK); for (int i = 0; i < 5; i++) { SpanElement star = DOM.createSpan().cast(); // add star element to the container DOM.insertChild(container, star, i); // subscribe on ONCLICK event DOM.sinkEvents(star, Event.ONCLICK); stars.add(star); } setElement(container); setStylePrimaryName(CLASSNAME); } // main method for handling events in GWT widgets @Override public void onBrowserEvent(Event event) { super.onBrowserEvent(event); switch (event.getTypeInt()) { // react on ONCLICK event case Event.ONCLICK: SpanElement element = event.getEventTarget().cast(); // if click was on the star int index = stars.indexOf(element); if (index >= 0) { int value = index + 1; // set internal value setValue(value); // notify listeners if (listener != null) { listener.starClicked(value); } } break; } } @Override public void setStylePrimaryName(String style) { super.setStylePrimaryName(style); for (SpanElement star : stars) { star.setClassName(style + "-star"); } updateStarsStyle(this.value); } // let application code change the state public void setValue(int value) { this.value = value; updateStarsStyle(value); } // refresh visual representation private void updateStarsStyle(int value) { for (SpanElement star : stars) { star.removeClassName(getStylePrimaryName() + "-star-selected"); } for (int i = 0; i < value; i++) { stars.get(i).addClassName(getStylePrimaryName() + "-star-selected"); } } }
A widget is a client-side class responsible for displaying the component in the web browser and handling events. It defines interfaces for working with the server side. In our case these are the
setValue()
method and theStarClickListener
interface. -
RatingFieldServerComponent
is a Vaadin component class. It defines an API for the server code, accessor methods, event listeners and data sources connection. Developers use the methods of this class in the application code.package com.company.ratingsample.web.toolkit.ui; import com.company.ratingsample.web.toolkit.ui.client.ratingfield.RatingFieldServerRpc; import com.company.ratingsample.web.toolkit.ui.client.ratingfield.RatingFieldState; import com.vaadin.ui.AbstractField; // the field will have a value with integer type public class RatingFieldServerComponent extends AbstractField<Integer> { public RatingFieldServerComponent() { // register an interface implementation that will be invoked on a request from the client registerRpc((RatingFieldServerRpc) value -> setValue(value, true)); } // field value type @Override public Class<? extends Integer> getType() { return Integer.class; } // define own state class @Override protected RatingFieldState getState() { return (RatingFieldState) super.getState(); } @Override protected RatingFieldState getState(boolean markAsDirty) { return (RatingFieldState) super.getState(markAsDirty); } // we need to refresh the state when setValue is invoked from the application code @Override protected void setInternalValue(Integer newValue) { super.setInternalValue(newValue); if (newValue == null) { newValue = 0; } getState().value = newValue; } }
-
The
RatingFieldState
state class defines what data are sent between the client and the server. It contains public fields that are automatically serialized on server side and deserialized on the client.package com.company.ratingsample.web.toolkit.ui.client.ratingfield; import com.vaadin.shared.AbstractFieldState; public class RatingFieldState extends AbstractFieldState { { // change the main style name of the component primaryStyleName = "ratingfield"; } // define a field for the value public int value = 0; }
-
The
RatingFieldServerRpc
interface defines a server API that is used from the client-side. Its methods may be invoked by the RPC mechanism built into Vaadin. We will implement this interface in the component.package com.company.ratingsample.web.toolkit.ui.client.ratingfield; import com.vaadin.shared.communication.ServerRpc; public interface RatingFieldServerRpc extends ServerRpc { //method will be invoked in the client code void starClicked(int value); }
-
The
RatingFieldConnector
connector links client code with the server.package com.company.ratingsample.web.toolkit.ui.client.ratingfield; import com.company.ratingsample.web.toolkit.ui.RatingFieldServerComponent; import com.company.ratingsample.web.toolkit.ui.client.ratingfield.RatingFieldServerRpc; import com.company.ratingsample.web.toolkit.ui.client.ratingfield.RatingFieldState; import com.vaadin.client.communication.StateChangeEvent; import com.vaadin.client.ui.AbstractFieldConnector; import com.vaadin.shared.ui.Connect; // link the connector with the server implementation of RatingField // extend AbstractField connector @Connect(RatingFieldServerComponent.class) public class RatingFieldConnector extends AbstractFieldConnector { // we will use a RatingFieldWidget widget @Override public RatingFieldWidget getWidget() { RatingFieldWidget widget = (RatingFieldWidget) super.getWidget(); if (widget.listener == null) { widget.listener = new RatingFieldWidget.StarClickListener() { @Override public void starClicked(int value) { getRpcProxy(RatingFieldServerRpc.class).starClicked(value); } }; } return widget; } // our state class is RatingFieldState @Override public RatingFieldState getState() { return (RatingFieldState) super.getState(); } // react on server state change @Override public void onStateChanged(StateChangeEvent stateChangeEvent) { super.onStateChanged(stateChangeEvent); // refresh the widget if the value on server has changed if (stateChangeEvent.hasPropertyChanged("value")) { getWidget().setValue(getState().value); } } }
The RatingFieldWidget
class does not define the component appearance, it only assigns style names to key elements. To define an appearance of the component, we’ll create stylesheet files. Click Manage themes → Create theme extension on the Project properties navigator section. Select the halo
theme in the dialog. Studio creates SCSS files for the theme extension in the themes
directory of the web module. The halo
theme uses FontAwesome font glyphs instead of icons. We’ll use this fact.
It is recommended to put component styles into a separate file componentname.scss
in the components/componentname
directory in the form of SCSS mixture. Create the components/ratingfield
directories structure in the themes/halo
directory of the web module. Then create the ratingfield.scss
file inside the ratingfield
directory:
@mixin ratingfield($primary-stylename: ratingfield) {
.#{$primary-stylename}-star {
font-family: FontAwesome;
font-size: $v-font-size--h2;
padding-right: round($v-unit-size/4);
cursor: pointer;
&:after {
content: '\f006'; // 'fa-star-o'
}
}
.#{$primary-stylename}-star-selected {
&:after {
content: '\f005'; // 'fa-star'
}
}
.#{$primary-stylename} .#{$primary-stylename}-star:last-child {
padding-right: 0;
}
.#{$primary-stylename}.v-disabled .#{$primary-stylename}-star {
cursor: default;
}
}
Include this file in the halo-ext.scss
main theme file:
@import "components/ratingfield/ra0tingfield";
@mixin com_company_ratingsample-halo-ext {
@include ratingfield;
}
To demonstrate how the component works let’s create a new screen in the web module.
Name the screen file rating-screen.xml
.
Open the rating-screen.xml
file in the IDE. We need a container for our component. Declare it in the screen XML:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
caption="msg://caption"
class="com.company.ratingsample.web.screens.RatingScreen"
messagesPack="com.company.ratingsample.web.screens">
<layout expand="container">
<vbox id="container">
<!-- we'll add vaadin component here-->
</vbox>
</layout>
</window>
Open the RatingScreen.java
screen controller and add the code that puts the component to the screen.
package com.company.ratingsample.web.screens;
import com.company.ratingsample.web.toolkit.ui.RatingFieldServerComponent;
import com.haulmont.cuba.gui.components.AbstractWindow;
import com.haulmont.cuba.gui.components.BoxLayout;
import com.haulmont.cuba.web.gui.components.WebComponentsHelper;
import com.vaadin.ui.Layout;
import javax.inject.Inject;
import java.util.Map;
public class RatingScreen extends AbstractWindow {
@Inject
private BoxLayout container;
@Override
public void init(Map<String, Object> params) {
super.init(params);
com.vaadin.ui.Layout containerLayout = (Layout) WebComponentsHelper.unwrap(container);
RatingFieldServerComponent field = new RatingFieldServerComponent();
field.setCaption("Rate this!");
containerLayout.addComponent(field);
}
}
Start the application server and see the result.
4.7.4.5. Support for Custom Visual Components in CUBA Studio
This section describes how to integrate a new custom visual component into CUBA Studio. As a result of the integration, the component will be available on the component palette of the WYSIWYG screen layout designer. Developers will be able to drag and drop the component to the canvas and edit its properties in the properties panel.
Let’s walk through the process of integrating the stepper
component into Studio. Creation of this component was described in Integrating a Vaadin Component into the Generic UI.
Open the project containing the stepper
component.
Tip
|
If you didn’t create this project, you can still reproduce the steps listed below in a new project. In this case, you will see how Studio supports the component, but you won’t be able to run the application. |
Click the Extend Studio link on the Project properties navigator section.
In the Extend Studio screen, fill in the following fields:
-
Configuration name - a configuration identifier. Enter
stepper
. -
Component XML element - a component element name to use in screen XML descriptors. It is
stepper
in our case.The Component class name and Component model class name fields are filled automatically based on the value of the Component XML element. Leave the values as is.
-
Component namespace URI - a namespace from the XSD that describes the Generic UI component. If you’ve generated the new component with Studio, then you can take the value of this field from the
ui-component.xsd
file located in the gui module. -
Component namespace prefix - a prefix for the component XML element.
-
Standard properties - standard properties that should be available for editing in the component properties panel of the Studio screen layout designer.
Select
caption
,datasource
andproperty
checkboxes.Tipid
,align
,height
,width
,enable
,stylename
andvisible
properties are available to any component by default. -
Custom properties - this table is used for declaring component specific properties that should be edited in the component properties panel.
Add the following properties:
-
manualInput of type
Boolean
, the default value istrue
-
mouseWheel of type
Boolean
, the default value istrue
-
stepAmount, of type
Integer
, the default value is0
-
maxValue, of type
Integer
, the default value is0
-
minValue, of type
Integer
, the default value is0
-
Press the OK button.
The custom visual components support is initialized when the Studio server start. Go to the Studio server window, stop the server, exit Studio, then reopen and start it again.
Re-create standard screens for the Customer
entity to erase the results of our previous experiments.
Then go to the GENERIC UI navigator section and open the customer-edit.xml
screen.
Remove the score
field from fieldGroup
because we will use a separate component for editing the score.
Find the new Stepper
component on the components palette, then drag it to the screen below fieldGroup
.
Select the stepper
component and go to the component Properties tab.
Fill in the fields:
-
id -
stepper
-
caption -
Stepper
-
datasource -
customerDs
-
property -
score
-
maxValue -
50
Go to the XML tab to see the result.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
caption="msg://editCaption"
class="com.company.addonguidemo.gui.customer.CustomerEdit"
datasource="customerDs"
focusComponent="fieldGroup"
messagesPack="com.company.addonguidemo.gui.customer"
xmlns:app="http://schemas.company.com/agd/0.1/ui-component.xsd">
<dsContext>
<datasource id="customerDs"
class="com.company.addonguidemo.entity.Customer"
view="_local"/>
</dsContext>
<layout expand="windowActions"
spacing="true">
<fieldGroup id="fieldGroup"
datasource="customerDs">
<column width="250px">
<field property="name"/>
</column>
</fieldGroup>
<app:stepper id="stepper"
caption="Stepper"
datasource="customerDs"
maxValue="50"
property="score"/>
<frame id="windowActions"
screen="editWindowActions"/>
</layout>
</window>
There is a new namespace with the app
prefix in the screen XML, the stepper
component is added to the screen, and its properties are set correctly.
5. The Framework
This chapter contains detailed description of the platform architecture, components and mechanisms.
5.1. Architecture
This section covers the architecture of CUBA applications in different perspectives: by tiers, blocks, modules, and components.
5.1.1. Application Tiers and Blocks
The platform enables building applications according to the classic three-tier pattern: client tier, middleware, database. The tier indicates the degree of "remoteness" from the stored data.
Further on, mainly middleware and client tiers will be described, therefore the words "all tiers" will refer to these tiers only.
Each tier enables creating one or more application blocks. A block is a separate executable program interacting with other blocks in the application. CUBA platform tools enable the creation of blocks in the form of web or desktop applications.
- Middleware
-
The middle tier contains core business logic of the application and provides access to the database. It is represented by a separate web application running on Java EE Web Profile standard container. See Middleware Components.
- Web Client
-
A main block of the client tier. It contains the interface designed primarily for internal users. It is represented by a separate web application running on Java EE Web Profile standard container. The user interface is implemented on the base of Vaadin framework. See Generic User Interface.
- Desktop Client
-
An additional block of the client tier. It contains the interface designed primarily for internal users. It is represented by a desktop Java application; the user interface is implemented on the base of Java Swing framework. See Generic User Interface.
- Web Portal
-
An additional block of the client tier. It can contain an interface for external users and entry points for integration with mobile devices and third-party applications. It is represented by a separate web application running on Java EE Web Profile standard container. The user interface is implemented on the base of Spring MVC framework. See Portal Components.
- Polymer Client
-
An optional client designed for external users and written in pure JavaScript. It is based on Google Polymer framework and communicates with the middleware via REST API running either in Web Client or in Web Portal blocks. See Polymer User Interface.
Middleware is the mandatory block for any application. User interface can be implemented by one or several blocks, such as Web Client and Polymer Client.
All of the Java-based client blocks interact with the middle tier uniformly via HTTP protocol enabling to deploy the middle tier arbitrarily, behind 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.
5.1.2. Application Modules
A module is the smallest structural part of 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. It is used only in Middleware.
-
gui – common components of the generic user interface. It is used in Web Client and Desktop Client.
-
web – the implementation of the generic user interface based on Vaadin and other specific web client classes. It is used in Web Client block.
-
desktop – an optional module – implementation of generic user interface based on Java Swing, as well as other specific desktop client classes. It is used in Desktop Client block.
-
portal – an optional module – implementation of Web portal based on Spring MVC.
-
polymer-client – an optional module – implementation of Polymer User Interface in JavaScript.
5.1.3. Application Components
The functionality of the platform is divided into several application components (aka base projects):
-
cuba – the main component containing all of the functionality described in this manual.
-
reports – reports generating subsystem.
-
fts – full-text search subsystem.
-
charts – subsystem for displaying charts and maps.
-
bpm – the mechanism of business processes execution according to the BPMN 2.0 standard.
An application always project depends on one ore more application components. It means that the application uses the component as a library and includes its functionality.
Any CUBA application depends on the cuba component. Other platform components are optional and can be included in the application only if needed. All optional components depend on cuba and can also contain dependencies between each other.
Below is a diagram showing dependencies between the platform application components. Solid lines demonstrate mandatory dependencies, dashed lines mean optional ones.
Any CUBA application can, in turn, be used as a component of another application. It enables decomposition of large projects to a set of functional modules, which can be developed independently. You can also create a set of custom application components in your organization and use them in multiple projects, effectively creating your own higher-level platform on top of CUBA. Below is an example of a possible structure of dependencies between application components.
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.
5.1.4. Application Structure
The above-listed architectural principles are directly reflected in the composition of assembled application. Let us consider the example of a simple application sales, which has two blocks – Middleware and Web Client; and includes the functionality of the two application components - cuba and reports.
The figure demonstrates the contents of several directories of the Tomcat server with a deployed application sales in it.
The Middleware block is represented by the app-core
web application, the Web Client block – by the app
web application. The executable code of the web applications can be found in directories WEB-INF/lib
in sets of JAR files. Each JAR (artifact) is a result of assembly of one of the application or a component modules.
For instance, the contents of JAR files of the web application in middle tier app-core
is determined by the facts that the Middleware block includes global and core modules, and the application uses cuba and reports components.
5.2. Common Components
This chapter covers platform components, which are common for all tiers of the application.
5.2.1. Data Model
Data model entities are divided into two categories:
-
Persistent – instances of such entities are stored in the database using ORM.
-
Non-persistent – instances exist only in memory, or are stored somewhere via different mechanisms.
The entities are characterized by their attributes. An attribute corresponds to a field and a pair of access methods (get / set) of the field. If the setter is omitted, the attribute becomes read-only.
Persistent entities may include attributes that are not stored in the database. For a non-persistent attribute, the field is optional and you can create only access methods.
The entity class should meet the following requirements:
-
Be inherited from one of the base classes provided by the platform (see below).
-
Have a set of fields and access methods corresponding to attributes.
-
The class and its fields (or access methods if the attribute has no corresponding field) must be annotated to provide information for the ORM (in case of a persistent entity) and metadata frameworks.
The following types can be used for entity attributes:
-
java.lang.String
-
java.lang.Boolean
-
java.lang.Integer
-
java.lang.Long
-
java.lang.Double
-
java.math.BigDecimal
-
java.util.Date
-
java.sql.Date
-
java.sql.Time
-
java.util.UUID
-
byte[]
-
enum
-
Entity
Base entity classes (see below) override equals()
and hashCode()
methods to check entity instances equivalence by comparing their identifiers. I.e., instances of the same class are considered equal if their identifiers are equal.
5.2.1.1. Base Entity Classes
The base entity classes and interfaces are described in detail in this section.
-
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
– extendsInstance
with entity identifier; at the same timeEntity
does not define the type of the identifier leaving this option to descendants. -
AbstractInstance
– implements the logic of working with attribute change listeners.WarningAbstractInstance
stores the listeners inWeakReference
, and if there are no external references to the added listener, it will be immediately destroyed by garbage collector. Normally, attribute change listeners are visual components and UI datasources that are always referenced by other objects, so there is no problem with listeners dropout. However, if a listener is created by application code and no objects refer to it in a natural way, it is necessary to save it in a certain object field apart from just adding it toInstance
. -
BaseGenericIdEntity
– base class of persistent and non-persistent entities. It implementsEntity
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.-
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 ofStandardEntity
. You can implement some of the interfacesCreatable
,Versioned
, etc. in your concrete entity class.
- BaseLongIdEntity
-
Inherit from
BaseLongIdEntity
orBaseIntegerIdEntity
if you want an entity with the primary key of theLong
orInteger
type. You can implement some of the interfacesCreatable
,Versioned
, etc. in your concrete entity class. ImplementingHasUuid
is highly recommended, as it enables some optimizations and allows you to identify your instances uniquely in a distributed environment.
- BaseStringIdEntity
-
Inherit from
BaseStringIdEntity
if you want an entity with the primary key of theString
type. You can implement some of the interfacesCreatable
,Versioned
, etc. in your concrete entity class. ImplementingHasUuid
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.
- BaseIdentityIdEntity
-
Inherit from
BaseIdentityIdEntity
if you need to map the entity to a table with IDENTITY primary key. You can implement some of the interfacesCreatable
,Versioned
, etc. in your concrete entity class. ImplementingHasUuid
is highly recommended, as it enables some optimizations and allows you to identify your instances uniquely in a distributed environment. Theid
attribute of the entity (i.e. getId/setId methods) will be of typeIdProxy
which is designed to substitute the real identifier until it is generated by the database on insert.
- BaseIntIdentityIdEntity
-
Inherit from
BaseIntIdentityIdEntity
if you need to map the entity to a table with IDENTITY primary key ofInteger
type (compared toLong
inBaseIdentityIdEntity
). In other respects,BaseIntIdentityIdEntity
is similar toBaseIdentityIdEntity
.
- BaseGenericIdEntity
-
Inherit from
BaseGenericIdEntity
directly if you need to map the entity to a table with a composite key. In this case, the concrete entity class must have a field of the embeddable type representing the key, annotated with the@EmbeddedId
JPA annotation.
5.2.1.2. Entity Annotations
This section describes all annotations of entity classes and attributes supported by the platform.
Annotations from the javax.persistence
package are required for JPA, annotations from com.haulmont.*
packages are designed for metadata management and other mechanisms of the platform.
In this manual, if an annotation is identified by a simple class name, it refers to a platform class, located in one of the com.haulmont.*
packages.
5.2.1.2.1. Class Annotations
- @Embeddable
-
Defines an embedded entity stored in the same table as the owning entity.
@MetaClass annotation should be used to specify the entity name.
- @EnableRestore
-
Indicates that the entity instances are available for recovery after soft deletion on the
core$Entity.restore
screen available through the Administration > Data Recovery main menu item.
- @Entity
-
Declares a class to be a data model entity.
Parameters:
-
name
– the name of the entity, must begin with a prefix, separated by a$
sign. It is recommended to use a short name of the project as a prefix to form a separate namespace.
Example:
@Entity(name = "sales$Customer")
-
- @Extends
-
Indicates that the entity is an extension and it should be used everywhere instead of the base entity. See Functionality Extension.
- @DiscriminatorColumn
-
Is used for defining a database column responsible for the distinction of entity types in the cases of
SINGLE_TABLE
andJOINED
inheritance strategies.Parameters:
-
name
– the discriminator column name -
discriminatorType
– the discriminator column type
Example:
@DiscriminatorColumn(name = "TYPE", discriminatorType = DiscriminatorType.INTEGER)
-
- @DiscriminatorValue
-
Defines the discriminator column value for this entity.
Example:
@DiscriminatorValue("0")
- @Inheritance
-
Defines the inheritance strategy to be used for an entity class hierarchy. It is specified on the entity class that is the root of the entity class hierarchy.
Parameters:
-
strategy
– inheritance strategy,SINGLE_TABLE
by default
-
- @Listeners
-
Defines the list of listeners intended for reaction to the entity instance lifecycle events on the middle tier.
The annotation value should be a string or an array of strings containing bean names of the listeners. See Entity Listeners.
Examples:
@Listeners("sample_UserEntityListener")
@Listeners({"sample_FooListener","sample_BarListener"})
- @MappedSuperclass
-
Defines that the class is an ancestor for some entities and its attributes must be used as part of descendant entities. Such class is not associated with any particular database table.
- @MetaClass
-
Is used for declaring non-persistent or embedded entity (meaning that
@javax.persistence.Entity
annotation cannot be applied)Parameters:
-
name
– the entity name, must begin with a prefix, separated by a$
sign. It is recommended to use a short name of the project as prefix to form a separate namespace.
Example:
@MetaClass(name = "sales$Customer")
-
- @NamePattern
-
Determines the way of getting the name of the instance returned by the
Instance.getInstanceName()
method.The annotation value should be a string in the
{0}|{1}
format, where:-
{0}
– formatting string according to theString.format()
rules, or this object method name with the prefix#
. The method should returnString
and should have no parameters. -
{1}
– a list of field names separated by commas, corresponding to{0}
format. If the method is used in{0}
, the list of fields is still required as it forms the_minimal
view.
Examples:
@NamePattern("%s|name")
@NamePattern("#getCaption|login,name")
-
- @PostConstruct
-
This annotation can be specified for a method. Such method will be invoked right after the entity instance is created by the Metadata.create() method. This is convenient when instance initialization requires invocation of managed beans. For example, see Entity Fields Initialization.
- @SystemLevel
-
Indicates that the entity is system only and should not be available for selection in various lists of entities, such as generic filter parameter types or dynamic attribute type.
- @Table
-
Defines database table for the given entity.
Parameters:
-
name
– the table name
Example:
@Table(name = "SALES_CUSTOMER")
-
- @TrackEditScreenHistory
-
Indicates that editor screens opening history will be recorded with the ability to display it on the
sec$ScreenHistory.browse
screen available through the Help > History main menu item.
5.2.1.2.2. Attribute Annotations
Attribute annotations should be set for the corresponding fields, with the following exception: if there is a need to declare read-only, non-persistent attribute foo
, it is sufficient to create getFoo()
method and annotate it with @MetaProperty
.
- @CaseConversion
-
Indicates that automatic case conversion should be used for text input fields bound with annotated entity attribute.
Parameters:
-
type
- the conversion type:UPPER
(default),LOWER
.
Example:
@CaseConversion(type = ConversionType.UPPER) @Column(name = "COUNTRY_CODE") protected String countryCode;
-
- @Column
-
Defines DB column for storing attribute values.
Parameters:
-
name
– the column name. -
length
– (optional parameter,255
by default) – the length of the column. It is also used for metadata generation and ultimately, can limit the maximum length of the input text in visual components bound to this attribute. Add the@Lob
annotation to remove restriction on the attribute length. -
nullable
– (optional parameter,true
by default) – determines if an attribute can containnull
value. Whennullable = false
JPA ensures that the field has a value when saved. In addition, visual components working with the attribute can request the user to enter a value.
-
- @Composition
-
Indicates that the relationship is a composition, which is a stronger variant of the association. Essentially this means that the related entity should only exist as a part of the owning entity, i.e. be created and deleted together with it.
For example, a list of items in an order (
Order
class contains a collection ofItem
instances):@OneToMany(mappedBy = "order") @Composition protected List<Item> items;
Another example is a one-to-one relationship:
@Composition @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "DETAILS_ID") protected CustomerDetails details;
Choosing
@Composition
annotation as the relationship type enables making use of a special commit mode for datasources in edit screens. In this mode, the changes to related instances are only stored when the master entity is committed. See Composite Structures for details.
- @Embedded
-
Defines a reference attribute of embeddable type. The referenced entity should have
@Embeddable
annotation.Example:
@Embedded protected Address address;
- @EmbeddedParameters
-
By default, ORM does not create an instance of embedded entity if all its attributes are null in the database. You can use the
@EmbeddedParameters
annotation to specify a different behavior when an instance is always non-null, for example:@Embedded @EmbeddedParameters(nullAllowed = false) protected Address address;
- @Id
-
Indicates that the attribute is the entity primary key. Typically, this annotation is set on the field of a base class, such as BaseUuidEntity. Using this annotation for a specific entity class is required only in case of inheritance from the
BaseStringIdEntity
base class (i.e. creating an entity with a string primary key).
- @IgnoreUserTimeZone
-
Makes the platform to ignore the user’s time zone (if it is set for the current session) for an attribute of the timestamp type (annotated with
@javax.persistence.Temporal.TIMESTAMP
).
- @JoinColumn
-
Defines DB column that determines the relationship between entities. Presence of this annotation indicates the owning side of the association.
Parameters:
-
name
– the column name
Example:
@ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "CUSTOMER_ID") protected Customer customer;
-
- @JoinTable
-
Defines a join table on the owning side of
@ManyToMany
relationship.Parameters:
-
name
– the join table name -
joinColumns
–@JoinColumn
element in the join table corresponding to primary key of the owning side of the relationship (the one containing@JoinTable
annotation) -
inverseJoinColumns
–@JoinColumn
element in the join table corresponding to primary key of the non-owning side of the relationship.
Example of the
customers
attribute of theGroup
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 theCustomer
class on non-owning side of the same relationship:@ManyToMany(mappedBy = "customers") protected Set<Group> groups;
-
- @Lob
-
Indicates that the attribute does not have any length restrictions. This annotation is used together with the
@Column
annotation. If@Lob
is set, the default or explicitly defined length in@Column
is ignored.Example:
@Column(name = "DESCRIPTION") @Lob private String description;
- @LocalizedValue
-
Determines a method for retrieving a localized value for an attribute, using MessageTools.
getLocValue()
method.Parameters:
-
messagePack
– explicit indication of the package, from which a localized message will be taken, for example,com.haulmont.cuba.core.entity
. -
messagePackExpr
– expression defining the path to the attribute, containing a package name from which the localized message should be taken (for example,proc.messagesPack
). The path starts from the attribute of the current entity.
The annotation in the example below indicates that localized message for the
state
attribute value should be taken from the package name defined in themessagesPack
attribute of theproc
entity.@Column(name = "STATE") @LocalizedValue(messagePackExpr = "proc.messagesPack") protected String state; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "PROC_ID") protected Proc proc;
-
- @Lookup
-
Defines the lookup type settings for the reference attributes.
Parameters:
-
type
- the default value isSCREEN
, so a reference is selected from a lookup screen. TheDROPDOWN
value enables to select the reference from a drop-down list. If the lookup type is set toDROPDOWN
, Studio will generate options datasource when scaffolding editor screen. Thus, the Lookup type parameter should be set before generation of an entity editor screen. Besides, the Filter component will allow a user to select parameter of this type from a drop-down list instead of lookup screen. -
actions
- defines the actions to be used in a PickerField component inside the FieldGroup by default. Possible values:lookup
,clear
,open
.
@Lookup(type = LookupType.DROPDOWN, actions = {"open"}) @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "CUSTOMER_ID") protected Customer customer;
-
- @ManyToMany
-
Defines a collection attribute with many-to-many relationship type.
Many-to-many relationship 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 – withmappedBy
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 remainLAZY
, since retrieval of referenced entities in CUBA-application is determined dynamically by the views mechanism.
WarningThe 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 toLAZY
, 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 containnull
value. Ifoptional = 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 sameCustomer
instance. In this case theOrder.customer
attribute should have the following annotations:@ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "CUSTOMER_ID") protected Customer customer;
WarningThe 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 containnull
value. Ifmandatory = true
, visual components working with this attribute can request the user to enter a value. -
datatype
- explicitly defines a datatype that overrides a datatype inferred from the attribute Java type. -
related
- defines the array of related persistent attributes to be fetched from the database when this property is included in a view.
Field example:
@Transient @MetaProperty protected String token;
Method example:
@MetaProperty public String getLocValue() { if (!StringUtils.isEmpty(messagesPack)) { return AppBeans.get(Messsages.class).getMessage(messagesPack, value); } else { return value; } }
-
- @NumberFormat
-
Specifies a format for an attribute of the
Number
type (it can beBigDecimal
,Integer
,Long
orDouble
). 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/orgroupingSeparator
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 remainLAZY
, since retrieval of referenced entities in CUBA-application is determined dynamically by the views mechanism.
For example, several
Item
instances refer to the sameOrder
instance using@ManyToOne
fieldItem.order
. In this case theOrder
class can contain a collection ofItem
instances:@OneToMany(mappedBy = "order") protected Set<Item> items;
WarningThe usage of JPA
cascade
andorphanRemoval
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. TheorphanRemoval
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 toLAZY
, 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 containnull
value. Ifoptional = 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
orLinkedHashSet
to get a predictable sequence of elements.Parameters:
-
value
– string, determines the order in the format:
orderby_list::= orderby_item [,orderby_item]* orderby_item::= property_or_field_name [ASC | DESC]
Example:
@OneToMany(mappedBy = "user") @OrderBy("createTs") protected List<UserRole> userRoles;
-
- @Temporal
-
Specifies the type of the stored value for
java.util.Date
attribute: date, time or date+time.Parameters:
-
value
– the type of the stored value:DATE
,TIME
,TIMESTAMP
Example:
@Column(name = "START_DATE") @Temporal(TemporalType.DATE) protected Date startDate;
-
- @Transient
-
Indicates that field is not stored in the database, meaning it is non-persistent.
The fields supported by JPA types (See http://docs.oracle.com/javaee/7/api/javax/persistence/Basic.html) are persistent by default, that is why
@Transient
annotation is mandatory for non-persistent attribute of such type.@MetaProperty annotation is required if
@Transient
attribute should be included in metadata.
- @Version
-
Indicates that the annotated field stores a version for optimistic locking support.
Such field is required when an entity class implements the
Versioned
interface (StandardEntity
base class already contains such field).Example:
@Version @Column(name = "VERSION") private Integer version;
5.2.1.3. Enum Attributes
The standard use of JPA for enum
attributes involves an integer database field containing a value obtained from the ordinal()
method. This approach may lead to the following issues with extending a system in production:
-
An entity instance cannot be loaded, if the value of the enum in the database does not equal to any
ordinal
value. -
It is impossible to add a new value between the existing ones, which is important when sorting by enumeration value (order by).
CUBA-style approach to solving these problems is to detach the value stored in the database from ordinal
value of the enumeration. In order to do this, the field of the entity should be declared with the type, stored in the database (Integer
or String
), while the access methods (getter / setter) should be created with the actual enumeration type.
Example:
@Entity(name = "sales$Customer")
@Table(name = "SALES_CUSTOMER")
public class Customer extends StandardEntity {
@Column(name = "GRADE")
protected Integer grade;
public CustomerGrade getGrade() {
return grade == null ? null : CustomerGrade.fromId(grade);
}
public void setGrade(CustomerGrade grade) {
this.grade = grade == null ? null : grade.getId();
}
...
}
In this case, the enumeration class can look like this:
public enum CustomerGrade implements EnumClass<Integer> {
PREMIUM(10),
HIGH(20),
MEDIUM(30);
private Integer id;
CustomerGrade(Integer id) {
this.id = id;
}
@Override
public Integer getId() {
return id;
}
public static CustomerGrade fromId(Integer id) {
for (CustomerGrade grade : CustomerGrade.values()) {
if (grade.getId().equals(id))
return grade;
}
return null;
}
}
For correct reflection in metadata, the enumeration class must implement the EnumClass
interface.
As the examples show, grade
attribute corresponds to the Integer
type value stored in the database, which is specified by the id
field of CustomerGrade
enumeration, namely 10
, 20
or 30
. At the same time, the application code and metadata framework use CustomerGrade
enum through access methods, which perform the actual conversion.
A call to getGrade()
method will simply return null
, if the value in the database does not correspond to any of the enumeration values. In order to add a new value, for example, HIGHER
, between HIGH
and PREMIUM
, it is sufficient to add new enumeration value with id = 15
, which ensures that sorting by Customer.grade
field remains correct.
The Integer
field type provides the ordered list of constants and enables sorting in JPQL and SQL queries (>
, <
, >=
, ⇐
, order
by
), not to mention the negligible issue of database space and performance. On the other hand, Integer
values are not self-explanatory in query results, that complicates debugging and using raw data from the database or in serialized formats. In this regard, the String
type is more convenient.
Enumerations can be created in CUBA Studio on the DATA MODEL tab (New → Enumeration). To be used as an entity attribute, choose ENUM
in the Attribute type field of the attribute editor and select the Enumeration class in the Type field. Enumeration values can be associated with localized names that will be displayed in the user interface of the application.
5.2.1.4. Soft Deletion
CUBA platform supports soft deletion mode, when the records are not deleted from the database, but instead, marked in a special way, so that they become inaccessible for common use. Later, these records can be either completely removed from the database using some kind of scheduled procedure or restored.
Soft deletion mechanism is transparent for an application developer, the only requirement is for entity class to implement SoftDelete
interface. The platform will adjust data operations automatically.
Soft deletion mode offers the following benefits:
-
Significantly reduces the risk of data loss caused by incorrect user actions.
-
Enables making certain records inaccessible instantly even if there are references to them.
Using Orders-Customers data model as an example, let’s assume that a certain customer has made several orders but we need to make the customer instance inaccessible for users. This is impossible with traditional hard deletion, as deletion of a customer instance requires either deletion of all related orders or setting to null all references to the customer (meaning data loss). After soft deletion, the customer instance becomes unavailable for search and modification; however, a user can see the name of the customer in the order editor, as deletion attribute is purposely ignored when the related entities are fetched.
The standard behavior above can be modified with related entities processing policy.
The deleted entity instances can be manually restored on the Restore Deleted Entities screen available from the Administration menu of an application. This functionality is designed only for application administrators supposed to have all permissions to all entities, and should be used carefully, so it is recommended to deny access to this screen for simple users.
The negative impact of soft deletion is increase in database size and likely need for additional cleanup procedures.
5.2.1.4.1. Use of Soft Deletion
To support soft deletion, the entity class should implement SoftDelete
interface, and the corresponding database table should contain the following columns:
-
DELETE_TS
– when the record was deleted. -
DELETED_BY
– the login of the user who deleted the record.
The default behavior for instances implementing SoftDelete
interface, is that soft deleted entities are not returned by queries or search by id. If required, this behavior can by dynamically turned off using the following methods:
-
Calling
setSoftDeletion(false)
for the current EntityManager instance. -
Calling
setSoftDeletion(false)
forLoadContext
object when requesting data via DataManager. -
On the datasource level – calling
CollectionDatasource.setSoftDeletion(false)
or settingsoftDeletion="false"
attribute ofcollectionDatasource
element in the screen’s XML-descriptor.
In soft deletion mode, the platform automatically filters out the deleted instances when loading by identifier and when using JPQL queries, as well as the deleted elements of the related entities in collection attributes. However, related entities in single-value (*ToOne) attributes are loaded, regardless of whether the related instance was deleted or not.
5.2.1.4.2. Related Entities Processing Policy
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 notnull
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:
-
Prohibit deletion of entity with references:
DeletePolicyException
will be thrown if you try to deleteCustomer
instance, which is referred to by at least oneOrder
.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.
-
-
Cascade deletion of related collection elements: deletion of
Role
instance causes allPermission
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;
-
Disconnect the links with related collection elements: deletion of
Role
instance leads to setting to null references to thisRole
for allPermission
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:
-
Related entities policy is processed on Middleware when saving entities implementing
SoftDelete
to the database. -
Be careful when using
@OnDeleteInverse
together withCASCADE
andUNLINK
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 onJob.customer
attribute in aCustomer
–Job
association with many jobs to one customer, if you set@OnDeleteInverse(CASCADE)
policy onJob.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 than0
, an exception is thrown. This makes use of@OnDeleteInverse(DENY)
suitable forJob.customer
attribute.
5.2.1.4.3. Unique Constraints at Database Level
In order to apply unique constraints for certain value in the soft deletion mode, at least one non-deleted record with this value and an arbitrary number of deleted records with the same value may exist in database.
This logic can be implemented in a specific way for each database server type:
-
If database server supports partial indexes (e.g. PostgreSQL), unique restrictions can be achieved as follows:
create unique index IDX_SEC_USER_UNIQ_LOGIN on SEC_USER (LOGIN_LC) where DELETE_TS is null
-
If database server does not support partial indexes (e.g. Microsoft SQL Server 2005), DELETE_TS field can be included in the unique index:
create unique index IDX_SEC_USER_UNIQ_LOGIN on SEC_USER (LOGIN_LC, DELETE_TS)
5.2.2. Metadata Framework
Metadata framework is used to support efficient work with data model in CUBA-applications. The framework:
-
provides API for obtaining information about entities, their attributes and relations between the entities; it is also used for traversing object graphs;
-
serves as a specialized and more convenient alternative for Java Reflection API;
-
controls permitted data types and relationships between entities;
-
enables implementation of universal mechanisms for operations with data.
5.2.2.1. Metadata Interfaces
Let’s consider the basic metadata interfaces.
- 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 returnnull
whilegetClassNN()
(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 returnsnull
, 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 returnedMetaPropertyPath
object enables accessing the required (the last in the path) attribute by invokinggetMetaProperty()
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 thisMetaClass
. -
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 thanASSOCIATION
. For example, the relationship between Order and its Items is aCOMPOSITION
, as the Item cannot exist without the Order to which it belongs.The type of
ASSOCIATION
orCOMPOSITION
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()
– returnstrue
for simple type attribute. -
asDatatype()
– returns Datatype for simple type attribute. -
isEnum()
– returnstrue
for enumeration type attribute. -
asEnumeration()
– returns Enumeration for enumeration type attribute. -
isClass()
– returnstrue
for reference attribute ofASSOCIATION
orCOMPOSITION
type. -
asClass()
– returns metaclass of associated entity for a reference attribute. -
isOrdered()
– returnstrue
if the attribute is represented by an ordered collection (for exampleList
). -
getCardinality()
– relation kind of the reference attribute:ONE_TO_ONE
,MANY_TO_ONE
,ONE_TO_MANY
,MANY_TO_MANY
.
5.2.2.2. Metadata Building
The main source for metadata structure generation are annotated entity classes.
Entity class will be present in the metadata in the following cases:
-
Persistent entity class is annotated by
@Entity
,@Embeddable
,@MappedSuperclass
and is located within the root package specified in metadata.xml. -
Non-persistent entity class is annotated by
@MetaClass
and is located within the root package specified inmetadata.xml
.
All entities inside same root package are put into the same MetaModel
instance, which is given the name of this package. Entities within the same MetaModel
can contain arbitrary references to each other. References between entities from different meta-models can be created in the order of declaration of metadata.xml
files in cuba.metadataConfig property.
Entity attribute will be present in metadata if:
-
A class field is annotated by
@Column
,@OneToOne
,@OneToMany
,@ManyToOne
,@ManyToMany
,@Embedded
. -
A class field or an access method (getter) is annotated by
@MetaProperty
.
Metaclass and metaproperty parameters are determined on the base of the listed annotations parameters as well as field types and class methods. Besides, if an attribute does not have write access method (setter), it becomes immutable (read-only).
5.2.2.3. Datatype
Datatype
interface 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 getBigDecimalDatatype
@Column(name = "AMOUNT") private BigDecimal amount;
because
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 theyear
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 inmetadata.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.
5.2.2.3.1. Datatype Format Strings
Locale-dependent parsing formats are provided in the main messages pack of the application or its components, in the strings with the following keys:
-
numberDecimalSeparator
– decimal separator for numeric types. -
numberGroupingSeparator
– thousands separator for numeric types. -
integerFormat
– format forInteger
andLong
types. -
doubleFormat
– format forDouble
type. -
decimalFormat
– format forBigDecimal
type. -
dateTimeFormat
– format forjava.util.Date
type. -
dateFormat
– format forjava.sql.Date
type. -
timeFormat
– format forjava.sql.Time
type. -
trueString
– string corresponding toBoolean.TRUE
. -
falseString
– string corresponding toBoolean.FALSE
.
Tip
|
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.
5.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;
5.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.
5.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.propertiescoordinateFormat = #,##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); // ... }
5.2.2.4. Meta-Annotations
Entity meta-annotations are a set of key/value pairs providing additional information about entities.
Meta-annotations are accessed using meta-class getAnnotations()
method.
The sources of meta-annotations are:
-
@OnDelete
,@OnDeleteInverse
,@Extends
annotations. These annotations cause creation of special meta-annotations for describing relations between entities. -
Extendable meta-annotations marked with
@MetaAnnotation
. These annotations are converted to meta-annotations with a key corresponding to the full name of Java class of the annotation and a value which is a map of annotation attributes. For example,@TrackEditScreenHistory
annotation will have a value which is a map with a single entry:value → true
. The platform provides the following annotations of this kind:@NamePattern
,@SystemLevel
,@EnableRestore
,@TrackEditScreenHistory
. In your application or application components, you can create your own annotation classes and mark them with@MetaAnnotation
annotation. -
Optional: entity meta-annotations can also be defined in metadata.xml files. If a meta-annotation in XML has the same name as the meta-annotation created by Java entity class annotation, then it will override the latter.
The example below shows how to override meta-annotations in
metadata.xml
:<metadata xmlns="http://schemas.haulmont.com/cuba/metadata.xsd"> <!-- ... --> <annotations> <entity class="com.company.customers.entity.Customer"> <annotation name="com.haulmont.cuba.core.entity.annotation.TrackEditScreenHistory"> <attribute name="value" value="true" datatype="boolean"/> </annotation> <property name="name"> <annotation name="length" value="200"/> </property> <property name="customerGroup"> <annotation name="com.haulmont.cuba.core.entity.annotation.Lookup"> <attribute name="type" class="com.haulmont.cuba.core.entity.annotation.LookupType" value="DROPDOWN"/> <attribute name="actions" datatype="string"> <value>lookup</value> <value>open</value> </attribute> </annotation> </property> </entity> <entity class="com.company.customers.entity.CustomerGroup"> <annotation name="com.haulmont.cuba.core.entity.annotation.EnableRestore"> <attribute name="value" value="false" datatype="boolean"/> </annotation> </entity> </annotations> </metadata>
5.2.3. Views
When retrieving entities from the database, we often face the question: how to ensure loading of related entities to the desired depth?
For example, you need to display the date and amount together with the Customer name in the Orders browser, which means that you need to fetch the related Customer instance. And for the Order editor screen, you need to fetch the collection of Items, in addition to that each Item should contain a related Product instance to display its name.
Lazy loading can not help in most cases because data processing is usually performed not in the transaction where the entities were loaded but, for example, on the client tier in UI. At the same time, it is unacceptable to apply eager fetching using entity annotations as it leads to constant retrieval of the entire graph of related entities which can be very large.
Another similar problem is the requirement to limit the set of local entity attributes of the loaded graph: for example, some entity can have 50 attributes, including BLOB, but only 10 attributes need to be displayed on the screen. In this case, why should we download 40 remaining attributes from the database, then serialize them and transfer to the client when it does not need them at the moment?
Views mechanism resolves these issues by retrieving from the database and transmitting to the client entity graphs limited by depth and by attributes. A view is a descriptor of the object graph required for a certain UI screen or data-processing operation.
Views processing is performed in the following way:
-
All relations in the data model are declared with lazy fetching property (
fetch = FetchType.LAZY
. See Entity Annotations). -
In the data loading process, the calling code provides required view together with JPQL query or entity identifier.
-
The so-called FetchGroup is produced on the base of the view – this is a special feature of EclipseLink framework lying in the base of the ORM layer. FetchGroup affects the generation of SQL queries to the database: both the list of returned fields and joins with other tables containing related entities.
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 eithernull
or a unique name within all views for the entity. -
properties
– collection ofViewProperty
instances corresponding to the entity attributes that should be loaded. -
includeSystemProperties
– if set, system attributes (defined by basic interfaces of persistent entities, such asBaseEntity
andUpdatable
) 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 theFetchMode
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, theAUTO
mode is applied. If the reference represents a cacheable entity,UNDEFINED
will be used regardless of the value specified in the view. -
Tip
|
Regardless of the attributes defined in the view, the following attributes are always loaded:
|
Warning
|
An attempt to get or set a value for a not loaded attribute (not included into a view) raises an exception. You can check whether the attribute was loaded using the |
5.2.3.1. Views Creation
A view can be created in two possible ways:
-
Programmatically – by creating a
View
instance, for example:View view = new View(Order.class) .addProperty("date") .addProperty("amount") .addProperty("customer", new View(Customer.class) .addProperty("name") );
Typically, this way can be appropriate for creating views that are used in a single piece of business logic.
-
Declaratively – by creating an XML descriptor and deploying it to
ViewRepository
.View
instances are created and cached when the XML descriptor is deployed. Further on, the required view can be retrieved in any part of the application code by a call toViewRepository
providing the entity class and the view name.
Let us consider in details the declarative way for creation and working with views.
ViewRepository
is a Spring bean, accessible to all application blocks. The reference to ViewRepository
can be obtained using injection or through the Metadata infrastructure interface. ViewRepository.getView()
methods are used to retrieve view instances from the repository. deployViews()
methods from AbstractViewRepository
basic implementation are used to deploy XML descriptors to the repository.
Three views named _local
, _minimal
and _base
are available in the views repository for each entity by default:
-
_local
contains all local entity attributes. -
_minimal
contains the attributes which are included to the name of the entity instance and specified in the @NamePattern annotation. If the@NamePattern
annotation is not specified at the entity, this view does not contain any attributes. -
_base
includes all local non-system attributes and attributes defined by@NamePattern
(effectively_minimal
+_local
).
The detailed structure of view XML descriptors is explained here.
The example below shows a view descriptor for the Order
entity which provides loading of all local attributes, associated Customer
and the Items
collection:
<view class="com.sample.sales.entity.Order"
name="order-with-customer"
extends="_local">
<property name="customer" view="_minimal"/>
<property name="items" view="itemInOrder"/>
</view>
The recommended way of grouping and deployment of view descriptors is as follows:
-
Create views.xml file in the
src
root of the global module and place all view descriptors that should be globally accessible (i.e. on all application tiers) into it. -
Register this file in the cuba.viewsConfig application property of all blocks, i.e. in
app.properties
of the core module,web-app.properties
of the web module, etc. This will ensure automatic deployment of the views upon application startup into the repository. -
If there are views which are used only in one application block, they can be specified in the similar file of this block, for example,
web-views.xml
, and registered in cuba.viewsConfig property of this block only.If the repository contains a view with certain name for some entity, an attempt to deploy another view with this name for the same entity will be ignored. If you need to replace the existing view in the repository with a new one and guarantee its deployment, specify
overwrite = "true"
attribute for it.
Tip
|
It is recommended to give descriptive names to the views. For example, not just "browse", but "customerBrowse". It simplifies the search of views in XML descriptors. |
5.2.4. Managed Beans
Managed Beans are program components intended for implementation of the application’s business logic. "Managed" in this case means that the instance creation and dependency management is handled by the container, which is the main part of the Spring framework.
Tip
|
Managed Bean is a singleton by default, i.e., only one instance of such class exists in each application block. If a singleton bean contains mutable data in fields (in other words, has a state), it is necessary to synchronize access to such data. |
5.2.4.1. Creating a Bean
To create a managed bean, add the @org.springframework.stereotype.Component
annotation to the Java class. For example:
package com.sample.sales.core;
import com.sample.sales.entity.Order;
import org.springframework.stereotype.Component;
@Component(OrderWorker.NAME)
public class OrderWorker {
public static final String NAME = "sales_OrderWorker";
public void calculateTotals(Order order) {
}
}
It is recommended to assign a unique name to the bean in the {project_name}_{class_name}
form and to define it in the NAME
constant.
Tip
|
The |
The managed bean class should be placed inside the package tree with the root specified in the context:component-scan
element of the spring.xml file. In this case, the spring.xml
file contains the element:
<context:component-scan base-package="com.sample.sales"/>
which means that the search for annotated beans for this application block will be performed starting with the com.sample.sales
package.
Managed beans can be created on any tier, because the Spring Framework container is used in all standard blocks of the application.
5.2.4.2. Using the Bean
A reference to the bean can be obtained through injection or through the AppBeans
class. As an example of using the bean, let us look at the implementation of the OrderService
bean that delegates the execution to the OrderWorker
bean:
package com.sample.sales.core;
import com.haulmont.cuba.core.Persistence;
import com.sample.sales.entity.Order;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.inject.Inject;
@Service(OrderService.NAME)
public class OrderServiceBean implements OrderService {
@Inject
protected Persistence persistence;
@Inject
protected OrderWorker orderWorker;
@Transactional
@Override
public BigDecimal calculateTotals(Order order) {
Order entity = persistence.getEntityManager().merge(order);
orderWorker.calculateTotals(entity);
}
}
In this example, the service starts a transaction, merges the detached entity obtained from the client level into the persistent context, and passes the control to the OrderWorker
bean, which contains the main business logic.
5.2.5. JMX Beans
Sometimes, it is necessary to give system administrator an ability to view and change the state of some managed bean at runtime. In such case, it is recommended to create a JMX bean – a program component having the JMX interface. JMX bean is usually a wrapper delegating calls to the managed bean which actually maintains state: cache, configuration data or statistics.
As you can see from the diagram, the JMX bean consists of the interface and the implementation class. The class should be a managed bean, i.e., should have the @Component
annotation and unique name. The interface of the JMX bean is registered in spring.xml in a special way to create the JMX interface in the current JVM.
Calls to all JMX bean interface methods are intercepted using Spring AOP by the MBeanInterceptor
interceptor class, which sets the correct ClassLoader
in the current thread and enables logging of unhandled exceptions.
Warning
|
The JMX bean interface name must conform to the following format: |
JMX-interface can be utilized by external tools, such as jconsole or jvisualvm. In addition, the Web Client platform block includes the JMX console, which provides the basic tools to view the status and call the methods of the JMX beans.
5.2.5.1. Creating a JMX Bean
The following example shows how to create a JMX bean.
-
JMX bean interface:
package com.sample.sales.core; import org.springframework.jmx.export.annotation.*; @ManagedResource(description = "Performs operations on Orders") public interface OrdersMBean { @ManagedOperation(description = "Recalculates an order amount") @ManagedOperationParameters({@ManagedOperationParameter(name = "orderId", description = "")}) String calculateTotals(String orderId); }
-
The interface and its methods may contain annotations to specify the description of the JMX bean and its operations. This description will be displayed in all tools that work with this JMX interface, thereby helping the system administrator.
-
Optional
@JmxRunAsync
annotation is designed to denote long operations. When such operation is launched using the built-in JMX console, the platform displays a dialog with an indefinite progress bar and the Cancel button. A user can abort the operation and continue to work with the application. The annotation can also contain thetimeout
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.
WarningPlease 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 withString
, the following parameter types are supported:boolean
,double
,float
,int
,long
,Boolean
,Integer
.
-
-
The JMX bean class:
package com.sample.sales.core; import com.haulmont.cuba.core.*; import com.haulmont.cuba.core.app.*; import com.sample.sales.entity.Order; import org.apache.commons.lang.exception.ExceptionUtils; import org.springframework.stereotype.Component; import javax.inject.Inject; import java.util.UUID; @Component("sales_OrdersMBean") public class Orders implements OrdersMBean { @Inject protected OrderWorker orderWorker; @Inject protected Persistence persistence; @Authenticated @Override public String calculateTotals(String orderId) { try { try (Transaction tx = persistence.createTransaction()) { Order entity = persistence.getEntityManager().find(Order.class, UUID.fromString(orderId)); orderWorker.calculateTotals(entity); tx.commit(); }; return "Done"; } catch (Throwable e) { return ExceptionUtils.getStackTrace(e); } } }
The
@Component
annotation defines the class as a managed bean with thesales_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 thecatch
section. -
The method starts the transaction, loads the
Order
entity instance by identifier, and passes control to theOrderWorker
bean for processing.
-
-
The registration of the JMX bean in
spring.xml
:<bean id="sales_MBeanExporter" lazy-init="false" class="com.haulmont.cuba.core.sys.jmx.MBeanExporter"> <property name="beans"> <map> <entry key="${cuba.webContextName}.sales:type=Orders" value-ref="sales_OrdersMBean"/> </map> </property> </bean>
All JMX beans of a project are declared in one
MBeanExporter
instance in themap/entry
elements of thebeans
property. The key is JMX ObjectName, the value – the bean’s name specified in the@Component
annotation. ObjectName begins with the name of the web application, because several web applications, which export the same JMX interfaces, can be deployed into one application server instance (i.e., into one JVM).
5.2.5.2. The Platform JMX Beans
This section describes some of the JMX beans available in the platform.
5.2.5.2.1. CachingFacadeMBean
CachingFacadeMBean
provides methods to clear various caches in the Middleware and Web Client blocks.
JMX ObjectName: app-core.cuba:type=CachingFacade
and app.cuba:type=CachingFacade
5.2.5.2.2. ConfigStorageMBean
ConfigStorageMBean
enables viewing and setting values of the application properties in the Middleware, Web Client and Web Portal blocks.
This interface has separate sets of operations for working with properties stored in files (*AppProperties
) and stored in the database (*DbProperties
). These operations show only the properties explicitly set in the storage. It means that if you have a configuration interface defining a property and its default value, but you did not set the value in the database (or a file), these methods will not show the property and its current value.
Please note that the changes to property values stored in files are not persistent, and are valid only until restart of the application block.
Unlike the operations described above, the getConfigValue()
operation returns exactly the same value as the corresponding method of the configuration interface invoked in the application code.
JMX ObjectName:
-
app-core.cuba:type=ConfigStorage
-
app.cuba:type=ConfigStorage
-
app-portal.cuba:type=ConfigStorage
5.2.5.2.3. EmailerMBean
EmailerMBean enables viewing the current values of the email sending parameters, and sending test messages.
JMX ObjectName: app-core.cuba:type=Emailer
5.2.5.2.4. PersistenceManagerMBean
PersistenceManagerMBean provides the following abilities:
-
Managing entity statistics mechanism.
-
Viewing new DB update scripts using the
findUpdateDatabaseScripts()
method. Triggering DB update with theupdateDatabase()
method. -
Executing arbitrary JPQL queries in the Middleware context by using
jpqlLoadList()
,jpqlExecuteUpdate()
methods.
JMX ObjectName: app-core.cuba:type=PersistenceManager
5.2.5.2.5. ScriptingManagerMBean
ScriptingManagerMBean is the JMX facade for the Scripting infrastructure interface.
JMX ObjectName: app-core.cuba:type=ScriptingManager
JMX attributes:
-
RootPath
– absolute path to the configuration directory of the Middleware block, in which this bean was started.
JMX operations:
-
runGroovyScript()
– executes a Groovy script in the Middleware context and returns the result. The following variables are passed to the script:-
persistence
of the Persistence type. -
metadata
of the Metadata type. -
configuration
of the Configuration type. -
dataManager
of the DataManager type.The result type should be of the String type to be displayed in the JMX interface. Otherwise, the method is similar to the Scripting.runGroovyScript() method.
The example script for creating a set of test users is shown below:
import com.haulmont.cuba.core.* import com.haulmont.cuba.core.global.* import com.haulmont.cuba.security.entity.* PasswordEncryption passwordEncryption = AppBeans.get(PasswordEncryption.class) Transaction tx = persistence.createTransaction() try { EntityManager em = persistence.getEntityManager() Group group = em.getReference(Group.class, UUID.fromString('0fa2b1a5-1d68-4d69-9fbd-dff348347f93')) for (i in (1..250)) { User user = new User() user.setGroup(group) user.setLogin("user_${i.toString().padLeft(3, '0')}") user.setName(user.login) user.setPassword(passwordEncryption.getPasswordHash(user.id, '1')); em.persist(user) } tx.commit() } finally { tx.end() }
-
5.2.5.2.6. ServerInfoMBean
ServerInfoMBean provides the general information about this Middleware block: the build number, build date and the server id.
JMX ObjectName: app-core.cuba:type=ServerInfo
5.2.6. Infrastructure Interfaces
Infrastructure interfaces provide access to frequently used functionality of the platform. Most of them are located in the global module and can be used both on the middle and client tiers. However, some of them (Persistence, for example) are accessible only for Middleware code.
Infrastructure interfaces are implemented by Spring Framework beans, so they can be injected into any other managed components (managed beans, Middleware services, generic user interface screen controllers).
Also, like any other beans, infrastructure interfaces can be obtained using static methods of AppBeans
class, and can be used in non-managed components (POJO, helper classes etc.).
5.2.6.1. Configuration
The interface helps to obtain references to configuration interfaces.
Examples:
// field injection
@Inject
protected Configuration configuration;
...
String tempDir = configuration.getConfig(GlobalConfig.class).getTempDir();
// setter injection
protected GlobalConfig globalConfig;
@Inject
public void setConfiguration(Configuration configuration) {
this.globalConfig = configuration.getConfig(GlobalConfig.class);
}
// location
String tempDir = AppBeans.get(Configuration.class).getConfig(GlobalConfig.class).getTempDir();
5.2.6.2. Messages
Messages
interface provides methods to get localized message strings.
Let’s consider interface methods in detail.
-
getMessage()
– returns the localized message by key, pack name and required locale. There are several modifications of this method with different sets of parameters. If locale is not specified in the method parameter, the current user locale is used.Examples:
@Inject protected Messages messages; ... String message1 = messages.getMessage(getClass(), "someMessage"); String message2 = messages.getMessage("com.abc.sales.web.customer", "someMessage"); String message3 = messages.getMessage(RoleType.STANDARD);
-
formatMessage()
– retrieves a localized message by key, pack name and required locale, then uses it to format the input parameters. The format is defined according toString.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()
– returnsMessageTools
interface instance (see below).
5.2.6.2.1. MessageTools
MessageTools
interface is a managed bean containing additional methods for working with localized messages. You can access MessageTools
interface either using Messages.getTools()
method, or as any other bean – by means of injection or through AppBeans
class.
MessageTools
methods:
-
loadString()
– returns a localized message, specified by reference inmsg://{messagePack}/{key}
formatReference 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 toloadString()
as a separate parameter. -
{key}
– message key in the pack.
Examples of the message references:
msg://someMessage msg://com.abc.sales.web.customer/someMessage
-
-
getEntityCaption()
– returns the localized entity name. -
getPropertyCaption()
– returns the localized name of an entity attribute. -
hasPropertyCaption()
– checks whether the entity attribute was given a localized name. -
getLocValue()
– returns the localized value of the entity attribute based on @LocalizedValue annotation. -
getMessageRef()
– forms a message reference for meta-property which can be used to retrieve the localized name of the entity attribute. -
getDefaultLocale()
– returns default application locale, which is the first one listed in cuba.availableLocales application property. -
useLocaleLanguageOnly()
– returnstrue
, if for all locales supported by the application (defined incuba.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, ifuseLocaleLanguageOnly()
method returnstrue
.
You can override MessageTools
to extend the set of its methods in your application. Below are the examples of working with the extended interface:
MyMessageTools tools = messages.getTools();
tools.foo();
((MyMessageTools) messages.getTools()).foo();
5.2.6.3. Metadata
Metadata
interface provides access to metadata session and view repository.
Interface methods:
-
getSession()
– returns the metadata session instance. -
getViewRepository()
– returns the view repository instance. -
getExtendedEntities()
– returnsExtendedEntities
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
andBaseIntegerIdEntity
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()
– returnsMetadataTools
interface instance (see below).
5.2.6.3.1. MetadataTools
MetadataTools
is a managed bean, containing additional methods for working with metadata. You can access MetadataTools
interface by either using Metadata.getTools()
method, or as any other bean – by means of injection or through AppBeans
class.
`MetadataTools `methods:
-
getAllPersistentMetaClasses()
– returns the collection of persistent entities meta-classes. -
getAllEmbeddableMetaClasses()
– returns the collection of embeddable entities meta-classes. -
getAllEnums()
– returns the collection of enumeration classes used as entity attributes types. -
format()
– formats the passed value according to data type of the given meta-property. -
isSystem()
– checks if a meta-property is system, i.e. specified in one of the basic entity interfaces. -
isPersistent()
– checks if a meta-property is persistent, i.e. stored in the database. -
isTransient()
– checks if a meta-property or an arbitrary attribute is non-persistent. -
isEmbedded()
– checks if a meta-property is an embedded object. -
isAnnotationPresent()
– checks if an annotation is present on the class or on one of its ancestors. -
getNamePatternProperties()
– returns collection of meta-properties of attributes included in the instance name, returned byInstance.getInstanceName()
method. See @NamePattern.
You can override MetadataTools
bean in your application to extend the set of its methods. The examples of working with the extended interface:
MyMetadataTools tools = metadata.getTools();
tools.foo();
((MyMetadataTools) metadata.getTools()).foo();
5.2.6.4. Resources
Resources
interface maintains resources loading according to the following rules:
-
If the provided location is a URL, the resource is downloaded from this URL;
-
If the provided location begins with
classpath:
prefix, the resource is downloaded from classpath; -
If the location is not a URL and it does not begin with
classpath:
, then:-
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;
-
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()
– returnsInputStream
for the provided resource, ornull
, 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, ornull
, if the resource is not found.
5.2.6.5. Scripting
Scripting
interface is used to compile and load Java and Groovy classes dynamically (i.e. at runtime) as well as to execute Groovy scripts and expressions.
Scripting
methods:
-
evaluateGroovy()
– executes the Groovy expression and returns its result.cuba.groovyEvaluatorImport application property is used to define the common set of the imported classes inserted into each executed expression. By default, all standard application blocks import PersistenceHelper class.
The compiled expressions are cached, and this considerably speeds up repeated execution.
Example:
@Inject protected Scripting scripting; ... Integer intResult = scripting.evaluateGroovy("2 + 2", new Binding()); Binding binding = new Binding(); binding.setVariable("instance", new User()); Boolean boolResult = scripting.evaluateGroovy("return PersistenceHelper.isNew(instance)", binding);
-
runGroovyScript()
– executes Groovy script and returns its result.The script should be located either in application configuration folder or in classpath (the current
Scripting
implementation supports classpath resources within JAR files only). A script in the configuration folder overrides the script in classpath with the same name.The path to the script is constructed using separators
/
. The separator is not required in the beginning of the path.Example:
@Inject protected Scripting scripting; ... Binding binding = new Binding(); binding.setVariable("itemId", itemId); BigDecimal amount = scripting.runGroovyScript("com/abc/sales/CalculatePrice.groovy", binding);
-
loadClass()
– loads Java or Groovy class using the following steps:-
If the class is already loaded, it will be returned.
-
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. -
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. -
The compiled class is searched in classpath. If it is found, it will be loaded and returned.
-
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()
– returnsClassLoader
, which is able to work according to the rules forloadClass()
method described above.
Cache of the compiled classes can be cleaned at runtime using CachingFacadeMBean JMX bean.
See also ScriptingManagerMBean.
5.2.6.6. Security
This interface provides authorization – checking user access rights to different objects in the system. Most of the interface methods delegate to the corresponding methods of current UserSession object, but before this they search for an original meta-class of the entity, which is important for projects with extensions. Besides methods duplicating UserSession
functionality, this interface contains isEntityAttrReadPermitted()
and isEntityAttrUpdatePermitted()
methods that check attribute path availability with respect to availability of all attributes and entities included in the path.
The Security
interface is recommended to use everywhere instead of direct calling of the UserSession.isXYXPermitted()
methods.
See more in User Authentication.
5.2.6.7. TimeSource
TimeSource
interface provides the current time. Using new Date()
and similar methods in the application code is not recommended.
Examples:
@Inject
protected TimeSource timeSource;
...
Date date = timeSource.currentTimestamp();
long startTime = AppBeans.get(TimeSource.class).currentTimeMillis();
5.2.6.8. UserSessionSource
The interface is used to obtain current user session object. See more in User Authentication.
5.2.6.9. UuidSource
The interface is used to obtain UUID
values, including those used for entity identifiers. Using UUID.randomUUID()
in the application code is not recommended.
To call from a static context, you can use the UuidProvider
class, which also has an additional fromString()
method that works faster than the standard UUID.fromString()
method.
5.2.6.10. DataManager
DataManager
interface provides CRUD functionality on both middle and client tiers. It is a universal tool for loading entity graphs from the database and saving changed detached entity instances.
Tip
|
See DataManager vs. EntityManager for information on differences between DataManager and EntityManager. |
DataManager
in fact just delegates to a DataStore implementation and handles cross-database references if needed. The most implementation details described below are in effect when you work with entities stored in a relational database through the standard RdbmsStore
. For another type of data store, everything except the interface method signatures can be different. For simplicity, when we write DataManager without additional clarification, we mean DataManager via RdbmsStore.
DataManager
methods are listed below:
-
load(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") .properties("customer", "sum") .parameter("date", orderDate) .list();
-
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) .parameter("date", orderDate) .one();
-
load(LoadContext)
,loadList(LoadContext)
– load entities according to the parameters of theLoadContext
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 acceptsValueLoadContext
which defines a query for scalar values and a list of keys. The returned list contains instances ofKeyValueEntity
. 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 inRdbmsStore
executesselect count()
query with the same conditions as in the original query for maximum performance. -
commit(CommitContext)
– saves a set of entities passed inCommitContext
to the database. Collections of entities for updating and deletion must be specified separately.The method returns the set of entity instances returned by EntityManager.merge(); essentially these are fresh instances just updated in DB. Further work should be performed with these returned instances to prevent data loss or optimistic locking. You can ensure that required attributes are present in the returned entities by setting a view for each saved instance using
CommitContext.getViews()
map.Examples 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 theload()
method. -
remove(Entity)
- removes a specified instance from the database. Delegates tocommit()
method.
- Query
-
The rules for JPQL queries are similar to those described in Executing JPQL Queries. The difference is that the query executed via
DataManager
may only use named parameters; positional parameters are not supported.
- Transactions
-
DataManager always starts a new transaction and commits it on operation completion, thus returning entities in the detached state.
- 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 ofLoadContext
is set to false.
-
5.2.6.10.1. Security in DataManager
The load()
, loadList()
, loadValues()
and getCount()
methods check user’s READ permission for entities being loaded. Additionally, loading entities from the database is subject for access group constraints.
The commit()
method checks CREATE permissions for new entities, UPDATE for the updated entities and DELETE for the deleted ones.
By default, DataManager
checks permissions on entity operations (READ/CREATE/UPDATE/DELETE) when invoked from a client, and ignores them when invoked from a middleware code. Attribute permissions are not enforced by default.
If you want to check entity operation permissions when using DataManager
in your middleware code, obtain a wrapper via DataManager.secure()
method and call its methods. Alternatively, you can set the cuba.dataManagerChecksSecurityOnMiddleware application property to turn on security check for the whole application.
Attribute permissions will be enforced 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 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.
5.2.6.10.2. Queries with distinct
If a screen contains a table with paging, and JPQL that is used to load data can be modified at run time as a result of applying a generic filter or access group constraints, the following can happen when distinct
operator is omitted in JPQL queries:
-
If a collection is joined at the database level, the loaded dataset will contain duplicate rows.
-
On client level, the duplicates disappear in the datasource as they are added to a map (
java.util.Map
). -
In case of paged table, a page may show fewer lines than requested, while the total number of lines exceeds requested.
Thus, we recommend including distinct
in JPQL queries, which ensures the absence of duplicates in the dataset returned from the database. However, certain DB servers (PostgreSQL in particular) have performance problems when executing SQL queries with distinct
if the number of returned records is large (more than 10000).
To solve this, the platform contains a mechanism to operate correctly without distinct
at SQL level. This mechanism is enabled by cuba.inMemoryDistinct application property. When activated, it does the following:
-
The JPQL query should still include
select distinct
. -
DataManager
cutsdistinct
out of the JPQL query before sending it to ORM. -
After the data page is loaded by
DataManager
, it deletes the duplicates and runs additional queries to DB in order to retrieve the necessary number of rows which are then returned to the client.
5.2.6.10.3. Sequential Queries
DataManager
can select data from the results of previous requests. This capability is used by the generic filter for sequential application of filters.
The mechanism works as follows:
-
If a
LoadContext
with defined attributesprevQueries
andqueryKey
is provided,DataManager
executes the previous query and saves identifiers of retrieved entities in theSYS_QUERY_RESULT
table (corresponding tosys$QueryResult
entity), separating the sets of records by user sessions and the query session keyqueryKey
. -
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.
5.2.6.11. 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 returnstrue
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 returnstrue
, 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 asisLoaded()
but throwsIllegalArgumentException
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 asisLoadedWithView()
but throwsIllegalArgumentException
instead of returning false. -
makeDetached()
- accepts a newly created entity instance and turns it into the detached state. The detached object can be passed toDataManager.commit()
orEntityManager.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 toDataManager.commit()
orEntityManager.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.
5.2.6.11.1. PersistenceHelper
A helper class with static methods delegating to the EntityStates interface.
5.2.6.12. 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.
See also Spring Framework tutorial.
- 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 } }
TipBy default, Spring events require
protected
,package
orpublic
access modifiers for@EventListener
methods. Pay attention thatprivate
modifier is not supported.WarningMethods 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 useorder
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 theApplicationContext
. 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 thatUiEvent
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) { showNotification("User is removed " + event.getUser()); }
You can use
@Order
annotation for event listener ordering.If an event is
UiEvent
and fired using theEvents
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.
5.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
-
Desktop Client loader –
DesktopAppContextLoader
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"); } });
WarningPlease note that the recommended way to run code on the application startup and shutdown is using Application Lifecycle Events.
5.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 returnsfalse
. -
The
AppContext.isReady()
method returnsfalse
.
-
- AppContextStartedEvent
-
It is sent after
AppContextInitializedEvent
and after running allAppContext.Listener.applicationStarted()
. At this moment:-
The
AppContext.isStarted()
method returnstrue
. -
The
AppContext.isReady()
method returnsfalse
. -
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 returnsfalse
. -
The
AppContext.isReady()
method returnsfalse
.
-
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"); } }
5.2.9. Application Properties
Application properties represent named values of different types, which determine various aspects of application configuration and functionality. The platform uses application properties extensively, and you can also employ them to configure application-specific features.
Platform application properties can be classified by intended purpose as follows:
-
Configuration parameters – specify sets of configuration files and certain user interface parameters, i.e. determine the application functionality. Values of configuration parameters are usually defined for the application project at development time.
For example: cuba.springContextConfig.
-
Deployment parameters – describe various URLs to connect application blocks, DBMS type, security settings etc. Values of deployment parameters are usually depend on the environment where the application instance is installed.
For example: cuba.connectionUrlList, cuba.dbmsType, cuba.userSessionExpirationTimeoutSec.
-
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.
For example: cuba.entityLog.enabled, cuba.email.smtpHost.
- Setting Application Properties
-
Values of application properties can be set in the database, in the property files, or via Java system properties. Besides, a value set in a file overrides the value with the same name from the database. A value set as a Java system property overrides both values from files and from the database.
Some properties do not support setting values in the database for the following reason: their values are needed when the database is not accessible to the application code yet. These are configuration and deployment parameters mentioned above. So you can only define them in property files or via Java system properties. Runtime parameters can always be set in the database (and possibly be overridden by values in files or system properties).
Typically, an application property is used in one or several application blocks. For example, cuba.persistenceConfig is used only in Middleware, cuba.web.appWindowMode is used in Web Client, while cuba.springContextConfig is used in all blocks. It means that if you need to set some value to a property, you should do it in all blocks that use this property. Properties stored in the database are automatically available to all blocks, so you set them just in one place (in the database table) regardless of what blocks use them. Moreover, there is a standard UI screen to manage properties of this type: see Administration > Application Properties. Properties stored in files should be set separately in the respective files of the blocks.
TipWhen 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 theapp.properties
file of the core module and in theweb-app.properties
file of the web module. Deployment parameters can also be set outside of project files in the configuration directory. See Storing Properties in Files for details.
- Properties From Application Components
-
An application component can expose properties by defining them in its app-component.xml file. Then if an application which uses the component does not define its own value for the property, the value will be obtained from the component. If the application uses multiple components defining the same property, the actual value in the application will be obtained from the component which is the closest ancestor by the hierarchy of dependencies between components. If there are several components on the same level of the hierarchy, the value is unpredictable.
- Additive Properties
-
Sometimes it is needed to get a combined property value from all application components used in the project. This is especially true for configuration parameters that allow platform mechanisms to configure your application based on the parameters provided by components.
Such properties should be made additive by specifying the plus sign in the beginning of their values. This sign indicates that the property value will be assembled from application components at runtime. For example, cuba.persistenceConfig should be an additive property. In your project, it specifies a
persistence.xml
file defining your project’s data model. But due to the fact that the real property value will include alsopersistence.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.
-
-
5.2.9.1. Storing Properties in Files
Properties that determine configuration and deployment parameters are specified in special property files named according to the *app.properties
pattern. Each application block contains a set of such files which is defined as follows:
-
For web application blocks (Middleware, Web Client, Web Portal) the set of property files is specified in the
appPropertiesConfig
parameter of web.xml. -
For the Desktop Client block the standard way to specify the set of property files is to override the
getDefaultAppPropertiesConfig()
method in an application class inherited fromcom.haulmont.cuba.desktop.App
.
For example, the set of property files of the Middleware block is specified in the web/WEB-INF/web.xml
file of the core module and looks as follows:
<context-param>
<param-name>appPropertiesConfig</param-name>
<param-value>
classpath:com/company/sample/app.properties
/WEB-INF/local.app.properties
"file:${catalina.base}/conf/app-core/local.app.properties"
</param-value>
</context-param>
The classpath:
prefix means that the corresponding file can be found in the Java classpath, while file:
prefix means that it should be loaded from the file system. A path without such prefix means the path inside the web application relative to its root. Java system properties can be used: in this example, catalina.home
is the Tomcat installation path.
An order in which files are declared is important because the values, specified in each subsequent file override the values of the properties with the same name, specified in the preceding files.
The last file in the above set is local.app.properties
. It can be used to override application properties upon deployment. If the file does not exist, it is silently ignored. You can create this file on the application server and define all properties specific to the environment in it. As a result, the settings will be separated from the application, and you will be able to update the application without fear of losing the specific configuration information. The Using Tomcat in Production section contains an example of using the local.app.properties
file.
For Desktop Client, JVM command line arguments serve as an equivalent of local.app.properties
. In this block, the properties loader treats all the arguments containing "=" sign as a key/value pair and uses them to replace corresponding application properties specified in app.properties
files.
Tip
|
Use the following rules when create
|
5.2.9.2. Storing Properties in the Database
Application properties that represent runtime parameters are stored in the SYS_CONFIG
database table.
Such properties have the following distinctive features:
-
As the property value is stored in the database, it is defined in a single location, regardless of what application blocks use it.
-
The value can be changed and saved at runtime in the following ways:
-
Using the Administration > Application Properties screen.
-
Using the ConfigStorageMBean JMX bean.
-
If the configuration interface has a setter method, you can set the property value in the application code.
-
-
Property value can be overridden for a particular application block in its
*app.properties
file or via Java system property with the same name.
It is important to mention, that access to properties stored in the database on the client side leads to Middleware requests. This is less efficient than retrieving properties from local *app.properties
files. To reduce the number of requests, the client caches properties for the lifetime of configuration interface implementation instance. Thus, if you need to access the properties of a configuration interface from some UI screen for several times, it is recommended to get the reference to this interface upon screen initialization and save it to a screen controller field for further access.
5.2.9.3. Configuration Interfaces
The configuration interfaces mechanism enables working with application properties using Java interface methods, providing the following benefits:
-
Typed access – application code works with actual data types (String, Boolean, Integer etc.).
-
Instead of string property identifiers, the application code uses interface methods, which are checked by the compiler and you can use code completion when working in an IDE.
Example of reading the transaction timeout value in the Middleware block:
@Inject
private ServerConfig serverConfig;
public void doSomething() {
int timeout = serverConfig.getDefaultQueryTimeoutSec();
...
}
If injection is impossible, the configuration interface reference can be obtained via the Configuration infrastructure interface:
int timeout = AppBeans.get(Configuration.class)
.getConfig(ServerConfig.class)
.getDefaultQueryTimeoutSec();
Warning
|
Configuration interfaces are not regular Spring managed beans. They can only be obtained through explicit interface injection or via |
5.2.9.3.1. Using Configuration Interfaces
To create a configuration interface in your application, do the following:
-
Create an interface inherited from
com.haulmont.cuba.core.config.Config
(not to be confused with the entity classcom.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 theSystem.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 isSourceType.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).
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.
5.2.9.3.2. Property Types
The following property types are supported in the platform out-of-the-box:
-
String
, primitive types and their object wrappers (boolean
,Boolean
,int
,Integer
, etc.) -
enum
. The property value is stored in a file or in the database as the value name of the enumeration.If the enum implements the
EnumClass
interface and has the staticfromId()
method for getting a value by an identifier, you can specify that the enum identifier should be stored instead of value with the@EnumStore
annotation. For example:@Property("myapp.defaultCustomerGrade") @DefaultInteger(10) @EnumStore(EnumStoreMode.ID) CustomerGrade getDefaultCustomerGrade(); @EnumStore(EnumStoreMode.ID) void setDefaultCustomerGrade(CustomerGrade grade);
-
Persistent entity classes. When accessing a property of the entity type, the instance defined by the property value is loaded from the database.
To support arbitrary types, use TypeStringify
and TypeFactory
classes to convert the value to/from a string and specify these classes for the property with @Stringify
and @Factory
annotations.
Let us consider this process using the UUID
type as an example.
-
Create class
com.haulmont.cuba.core.config.type.UuidTypeFactory
inherited fromcom.haulmont.cuba.core.config.type.TypeFactory
and implement the following method in it:public Object build(String string) { if (string == null) { return null; } return UUID.fromString(string); }
-
There is no need to create
TypeStringify
astoString()
method is sufficient in this case. -
Annotate the property in the configuration interface:
@Factory(factory = UuidTypeFactory.class) UUID getUuidProp(); void setUuidProp(UUID value);
The platform provides TypeFactory
implementations for the following types:
-
UUID
–UuidTypeFactory
, as described above. -
java.util.Date
–DateFactory
. Date value must be specified inyyyy-MM-dd HH:mm:ss.SSS
format, for example:cuba.test.dateProp = 2013-12-12 00:00:00.000
-
List<Integer>
(the list of integers) –IntegerListTypeFactory
. The property value must be specified in the form of numbers, separated by spaces, for example:cuba.test.integerListProp = 1 2 3
-
List<String>
(the list of strings) –StringListTypeFactory
. The property value must be specified as a list of strings separated by "|" sign, for example:cuba.test.stringListProp = aaa|bbb|ccc
5.2.9.3.3. Default Values
You can specify default values for properties defined by configuration interfaces. These values will be returned instead of null
if the property is not set in the storage location – the database or *app.properties
files.
A default value can be specified as a string using the @Default
annotation, or as a specific type using other annotations from com.haulmont.cuba.core.config.defaults
package:
@Property("cuba.email.adminAddress")
@Default("address@company.com")
String getAdminAddress();
@Property("cuba.email.delayCallCount")
@Default("2")
int getDelayCallCount();
@Property("cuba.email.defaultSendingAttemptsCount")
@DefaultInt(10)
int getDefaultSendingAttemptsCount();
@Property("cuba.test.dateProp")
@Default("2013-12-12 00:00:00.000")
@Factory(factory = DateFactory.class)
Date getDateProp();
@Property("cuba.test.integerList")
@Default("1 2 3")
@Factory(factory = IntegerListTypeFactory.class)
List<Integer> getIntegerList();
@Property("cuba.test.stringList")
@Default("aaa|bbb|ccc")
@Factory(factory = StringListTypeFactory.class)
List<String> getStringList();
A default value for an entity is a string of the {entity_name}-{id}-{optional_view_name}
format, for example:
@Default("sec$User-98e5e66c-3ac9-11e2-94c1-3860770d7eaf-browse")
User getAdminUser();
@Default("sec$Role-a294aef0-3ac9-11e2-9433-3860770d7eaf")
Role getAdminRole();
5.2.10. Messages Localization
Applications based on CUBA platform support messages localization, which means that all user interface elements can be displayed in the language, selected by user.
Language selection options are determined by the combination of cuba.localeSelectVisible and cuba.availableLocales application properties.
This section describes the localization mechanism and rules of localized messages creation. For information about obtaining messages see Getting Localized Messages.
5.2.10.1. Message Packs
A message pack is a set of property files with the names in messages{_XX}.properties
format located in a single Java package. XX
suffix indicates the language of the messages in this file and corresponds to the language code in Locale.getLanguage()
. It is also possible to use other Locale
attributes, for example, country
. In this case the message pack file will look like messages{_XX_YY}.properties
. One of the files in the pack can have no language suffix – it is the default file. The name of the message pack corresponds to the name of the Java package, which contains the pack files.
Let us consider the following example:
/com/abc/sales/gui/customer/messages.properties
/com/abc/sales/gui/customer/messages_fr.properties
/com/abc/sales/gui/customer/messages_ru.properties
/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, whereXX
is the code of the required language. -
If there is no such file, default
messages.properties
file is searched in the same directory. -
If either the required language file or the default file is found, it is loaded together with all
@include
files, and the key message is searched in it. -
If the file is not found or it does not contain the proper key, the directory is changed to the parent one and the search procedure is repeated. The search continues until the root of the configuration directory is reached.
-
-
If the message is not found in the configuration directory, the search is performed in classpath according to the same algorithm.
-
If the message is found, it is cached and returned. If not, the fact that the message is not present is cached as well and the key which was passed for search is returned. Thus, the complex search procedure is only performed once and further on the result is loaded from the local cache of the application block.
Tip
|
It is recommended to organize message packs as follows:
|
5.2.10.2. Main Message Pack
Each standard application block should have its own main message pack. For the client tier blocks the main message pack contains main menu entries and common UI elements names (for example, names of OK and Cancel buttons). The main pack also determines Datatype transformation formats for all application blocks, including Middleware.
cuba.mainMessagePack application property is used to specify the main message pack. The property value can be either a single pack or list of packs separated by spaces. For example:
cuba.mainMessagePack=com.haulmont.cuba.web com.abc.sales.web
In this case the messages in the second pack of the list will override those from the first pack. Thus, the messages defined in the application components packs can be overridden in the application project.
Existing messages from CUBA base projects can be also overridden by specifying new messages in the project’s main message pack:
com.haulmont.cuba.gui.backgroundwork/backgroundWorkProgress.timeoutMessage = Overridden Error Message
5.2.10.3. Entity and Attributes Names Localization
To display localized names of the entities and attributes in UI, create special message packs in the Java packages containing the entities. Use the following format in message files:
-
Key of the entity name – simple class name (without package).
-
Key of the attribute name – simple class name, then the name of the attribute separated by period.
The example of default English localization of com.abc.sales.entity.Customer
entity – /com/abc/sales/entity/messages.properties
file:
Customer=Customer
Customer.name=Name
Customer.email=Email
Order=Order
Order.customer=Customer
Order.date=Date
Order.amount=Amount
Such message packs are usually used implicitly by the framework, for example, by Table and FieldGroup visual components. Besides, you can obtain the names of the entities and attributes using the following methods:
-
Programmatically – by MessageTools
getEntityCaption()
,getPropertyCaption()
methods; -
In XML screen descriptor – by reference to the message according to MessageTools.loadString() rules:
msg://{entity_package}/{key}
, for example:caption="msg://com.abc.sales.entity/Customer.name"
5.2.10.4. Enum Localization
To localize the enumeration names and values, add messages with the following keys to the message pack located in the Java package of the enumeration class:
-
Enumeration name key – simple class name (without package);
-
Value key – simple class name, then the value name separated by period.
For example, for enum
package com.abc.sales;
public enum CustomerGrade {
PREMIUM,
HIGH,
STANDARD
}
default English localization file /com/abc/sales/messages.properties
should contain the following lines:
CustomerGrade=Customer Grade
CustomerGrade.PREMIUM=Premium
CustomerGrade.HIGH=High
CustomerGrade.STANDARD=Standard
Localized enum values are automatically used by different visual components such as LookupField. You can obtain localized enum value programmatically: use getMessage()
method of the Messages interface and simply pass the enum
instance to it.
5.2.11. User Authentication
This section describes some access control aspects from the developer’s point of view. For complete information on configuring user data access restrictions, see Security Subsystem.
5.2.11.1. UserSession
User session is the main element of access control mechanism of CUBA applications. It is represented by the UserSession
object, which is associated with the currently authenticated user and contains information about user rights. The UserSession
object can be obtained in any application block using the UserSessionSource infrastructure interface.
The UserSession
object is created on Middleware during 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.
5.2.11.2. Login
CUBA Platform provides built-in extensible authentication mechanisms. They include different authentication schemes such as login/password, remember me, trusted and anonymous login.
This section primarily describes authentication mechanisms of the middle tier. For web client specifics, see Web Login.
The platform includes the following authentication mechanisms on middleware:
-
AuthenticationManager
implemented byAuthenticationManagerBean
-
AuthenticationProvider
implementations -
AuthenticationService
implemented byAuthenticationServiceBean
-
UserSessionLog
- see user session logging.
Also, it employs the following additional components:
-
TrustedClientService
implemented byTrustedClientServiceBean
- 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.
Standard user login process:
-
The user enters their username and password.
-
Application client invokes
Connection.login()
method passing the user login and password. -
Connection
createsCredentials
object and invokeslogin()
method ofAuthenticationService
. -
AuthenticationService
delegates execution to theAuthenticationManager
bean, which uses chain ofAuthenticationProvider
objects. There isLoginPasswordAuthenticationProvider
that can work withLoginPasswordCredentials
objects. It loadsUser
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.
See also Web Login Procedure.
Password hashing algorithm is implemented by the EncryptionModule
type bean and is specified in cuba.passwordEncryptionModule application property. SHA-1 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 ofAuthenticationManager.login()
is called.LoginPasswordAuthenticationProvider
,RememberMeAuthenticationProvider
andTrustedClientAuthenticationProvider
use additional pluggable checks: beans that implementUserAccessChecker
interface. If at least one of theUserAccessChecker
instances throwLoginException
then authentication is considered failed andLoginException
is thrown.Besides,
LoginPasswordAuthenticationProvider
andRememberMeAuthenticationProvider
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
andAuthenticationProvider
can throw LoginException or one of its descendants fromauthenticate()
andlogin()
methods. Also, UnsupportedCredentialsException is thrown if passed credentials object cannot be processed by availableAuthenticationProvider
beans.See the following exception classes:
-
UnsupportedCredentialsException
-
LoginException
-
AccountLockedException
-
UserIpRestrictedException
-
RestApiAccessDeniedException
-
- Events
-
Standard implementation of
AuthenticationManager
-AuthenticationManagerBean
fires the following application events during login / authentication procedure:-
BeforeAuthenticationEvent
/AfterAuthenticationEvent
-
BeforeLoginEvent
/AfterLoginEvent
-
AuthenticationSuccessEvent
/AuthenticationFailureEvent
-
UserLoggedInEvent
/UserLoggedOutEvent
-
UserSubstitutedEvent
Spring beans of the middle tier can handle these events using Spring
@EventListener
subscription:@Component public class LoginEventListener { @Inject private Logger log; @EventListener protected void onUserLoggedIn(UserLoggedInEvent event) { User user = event.getSource().getUser(); log.info("Logged in user {}", user.getInstanceName()); } }
Event handlers of all events mentioned above (excluding
AfterLoginEvent
,UserSubstitutedEvent
andUserLoggedInEvent
) can throwLoginException
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 existingAuthenticationServiceBean
. -
AuthenticationManager
- replace existingAuthenticationManagerBean
. -
AuthenticationProvider
implementations - implement additional or replace existingAuthenticationProvider
. -
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 useorder
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 withTrustedClientCredentials
. This method requires satisfying the following conditions:-
The client block has to pass the so-called trusted password, specified in the cuba.trustedClientPassword Middleware and client block application property.
-
IP address of the client block has to be in the list specified in the cuba.trustedClientPermittedIpList application property.
-
-
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()
withSystemUserCredentials
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.
-
- Obsolete/Deprecated
-
The following components now are considered deprecated:
-
LoginService
delegates login methods execution toAuthenticationService
-
LoginWorker
delegates login methods execution toAuthenticationManager
Do not use these components in your code. They will be removed in the next version of the platform.
-
5.2.11.3. SecurityContext
SecurityContext
class instance stores information about the user session for the current execution thread. It is created and passed to AppContext.setSecurityContext()
method in the following moments:
-
For the Web Client and Web Portal blocks – at the beginning of processing of each HTTP request from the user browser.
-
For the Middleware block – at the beginning of processing of each request from the client tier and from CUBA Scheduled Tasks.
-
For the Desktop Client block – once after the user login, as the desktop application is running in single user mode.
In the first two cases, SecurityContext
is removed from the execution thread when the request execution is finished.
If you create a new execution thread from the application code, pass the current SecurityContext
instance to it as in the example below:
final SecurityContext securityContext = AppContext.getSecurityContext();
executor.submit(new Runnable() {
public void run() {
AppContext.setSecurityContext(securityContext);
// business logic here
}
});
The same can be done using SecurityContextAwareRunnable
or SecurityContextAwareCallable
wrappers, for example:
executor.submit(new SecurityContextAwareRunnable<>(() -> {
// business logic here
}));
Future<String> future = executor.submit(new SecurityContextAwareCallable<>(() -> {
// business logic here
return some_string;
}));
5.2.12. Exceptions Handling
This section describes various aspects of working with exceptions in CUBA applications.
5.2.12.1. Exception Classes
The following rules should be followed when creating your own exception classes:
-
If the exception is part of business logic and requires some non-trivial actions to handle it, the exception class should be made checked (inherited from
Exception
). Such exceptions are handled by the invoking code. -
If the exception indicates an error and assumes interruption of execution and a simple action like displaying the error information to the user, its class should be unchecked (inherited from
RuntimeException
). Such exceptions are processed by special handler classes registered in the client blocks of the application. -
If the exception is thrown and processed in the same block, its class should be declared in corresponding module. If the exception is thrown on Middleware and processed on the client tier, the exception class should be declared in the global module.
The platform contains a special unchecked exception class SilentException
. It can be used to interrupt execution without showing any messages to the user or writing them to the log. SilentException
is declared in the global module, and therefore is accessible both in Middleware and client blocks.
5.2.12.2. Passing Middleware Exceptions
If an exception is thrown on Middleware as a result of handling a client request, the execution terminates and the exception object is returned to the client. The object usually includes the chain of underlying exceptions. This chain can contain classes which are inaccessible for the client tier (for example, JDBC driver exceptions). For this reason, instead of sending this chain to the client we send its representation inside a specially created RemoteException
object.
The information about the causing exceptions is stored as a list of RemoteException.Cause
objects. Each Cause
object always contains an exception class name and its message. Moreover, if the exception class is "supported by client", Cause
stores the exception object as well. This enables passing information to the client in the exception fields.
Exception class should be annotated by @SupportedByClient
if its objects should be passed to the client tier as Java objects. For example:
@SupportedByClient
public class WorkflowException extends RuntimeException {
...
Thus, when an exception is thrown on Middleware and it is not annotated by @SupportedByClient
the calling client code will receive RemoteException
containing original exception information in a string form. If the source exception is annotated by @SupportedByClient
, the caller will receive it directly. This enables handling the exceptions declared by Middleware services in the application code in the traditional way – using try/catch blocks.
Bear in mind that if you need the exception supported by client to be passed on the client as an object, it should not contain any unsupported exceptions in its getCause()
chain. Therefore, if you create an exception instance on Middleware and want to pass it to the client, specify cause parameter only if you are sure that it contains the exceptions known to the client.
ServiceInterceptor
class is a service interceptor which packs the exception objects before passing them to the client tier. Besides, it performs exceptions logging. All information about the exception including full stack trace is logged by default. If it is not desirable, add @Logging
annotation to the exception class and specify the logging level:
-
FULL
– full information, including stacktrace (default). -
BRIEF
– exception class name and message only. -
NONE
– no output.
For example:
@SupportedByClient
@Logging(Logging.Type.BRIEF)
public class FinancialTransactionException extends Exception {
...