- @PrimaryKeyJoinColumn
-
在
JOINED
继承策略的情况下用于为实体指定外键列,该外键是父类实体主键的引用。参数:
-
name
– 实体的外键列的名称 -
referencedColumnName
– 父类实体的主键列的名称
例如:
@PrimaryKeyJoinColumn(name = "CARD_ID", referencedColumnName = "ID")
-
序言
本手册提供关于 CUBA 框架的参考信息,并且介绍了使用此平台开发商业应用程序的最重要的内容。
如果要使用此平台,需要掌握以下技术知识:
-
Java SE
-
关系型数据库(SQL, DDL)
另外,掌握以下技术和框架对更深层次的了解此平台有很大帮助:
-
Gradle 构建系统
-
Spring Framework - Spring 框架
-
Java Persistence API - JPA
-
Vaadin web 应用程序框架
-
HTML / CSS / JavaScript
-
Java Servlets
如果有任何关于改进本手册的建议,请提交 issue 到 GitHub。如果发现拼写或者单词错误,缺陷或者不一致的地方,请直接 fork 这个仓库并提交修改。谢谢!
1. 安装
- 系统要求
-
-
64-bit 操作系统: Windows,Linux 或者 macOS
-
内存 – 至少 4GB,推荐 8GB
-
硬盘空间 – 至少 5 GB
-
- Java SE Development Kit (JDK)
-
-
安装 JDK 8 然后在终端运行下面命令检查 Java 版本:
java -version
这个命令会输出 Java 版本信息,比如:
1.8.0_202
。CUBA 7.0 支持 Java 8,9,10 和 11。如果不需要支持使用 CUBA 老版本创建的项目(包括迁移到 CUBA 7.0 的项目),那么可以直接安装 Java 11。
-
设置
JAVA_HOME
环境变量为 JDK 的根目录路径,比如:C:\Java\jdk8u202-b08
。-
在 Windows 系统,可以通过 Computer → Properties → Advanced System Settings → Advanced → Environment variables 来设置。这个变量的值需要加到 系统变量(System variables) 列表中。
-
在 macOS 系统,建议在
/Library/Java/JavaVirtualMachines
安装 JDK,比如/Library/Java/JavaVirtualMachines/jdk8u202-b08
,然后在~/.bash_profile
用下面的命令设置JAVA_HOME
:export JAVA_HOME="$(/usr/libexec/java_home -v 1.8)"
-
-
如果是通过代理服务器上网,在运行开发工具和 Gradle 的时候需要给 JVM 设置一些关于代理的系统参数。这些参数在 这里 有解释(可以查查 HTTP 和 HTTPS 协议的相关参数)。建议在系统级别通过
JAVA_OPTS
环境变量设置这些参数。
-
- 开发工具
-
下面这些工具有助于使用 CUBA 框架开发:
-
CUBA Studio - 构建在 IntelliJ 平台之上并且针对 CUBA 项目量身定做的集成开发环境。可以安装为系统的独立运行程序,或者作为 IntelliJ IDEA 的插件安装(社区版或者终极版都行)。更多信息参考 CUBA Studio 使用手册。
-
CUBA CLI - 一个命令行工具,能帮助创建项目和项目元素的基本脚手架代码:实体,界面,服务,等等。使用这个工具的话,可以用任何 Java IDE 开发 CUBA 应用程序。更多信息参考 CUBA CLI GitHub。
如果不熟悉 Java,推荐使用 CUBA Studio,因为这是最高级最直观的工具。
-
- 数据库
-
最基本的场景中,内置的 HyperSQL (http://hsqldb.org) 就可以用来做数据库服务。对于研究平台能力和做应用程序原型来说足够了。如果需要构建生产环境应用程序,还是推荐安装平台支持的全功能 DBMS,比如 PostgreSQL。
- 网页浏览器
-
基于平台开发的应用的 Web 界面支持所有流行的浏览器,包括 Google Chrome 、Mozilla Firefox 、 Safari 、 Opera 15+ 、 Internet Explorer 11 、 Microsoft Edge。
2. 快速开始
本章节介绍使用 CUBA Studio 创建应用程序的过程。
确保必要的软件已经安装并且配置好了,参考 安装。
开发应用程序的关键步骤:
-
开发数据模型 - 包括创建描述应用程序领域模型的实体以及相对应的数据库表。
-
开发用户界面 - 用来创建,查看,更改和删除数据模型实体。
2.1. 示例应用程序详情
此应用主要用来维护客户(customer)信息以及客户订单(order)信息。
customer 有如下属性:
-
Name
-
Email
order 属性:
-
指向 customer 的引用
-
Date
-
Amount
应用程序界面需要包含:
-
Customers 浏览界面;
-
Customer 编辑界面,也包含 customer 的 orders 列表;
-
General orders 浏览界面;
-
Order 编辑界面。
2.2. 创建项目
-
启动 CUBA Studio 。
-
点击 Create New Project 创建新项目。
-
确保安装了 Java SE Development Kit (JDK) 8,并且选择这个版本为项目默认的 JDK。
仓库(Repository)的列表已经包含了仓库 URL 以及认证需要的参数。
-
在 New project 窗口的 Project name 字段指定新项目名称 - 比如
sales
。名称只能包含拉丁字母,数字以及下划线。这里需要仔细考虑项目名称,因为之后想要修改的话,会需要比较麻烦的手动操作。-
下面这些字段会自动填充:
-
Project location – 项目目录的路径。您可以通过点击右边的 … 按钮来进行手动选择。点击之后会出现 Select folder 的窗口,这里会给出你硬盘上的目录列表。您可以选择一个或者创建一个新目录。
-
Project namespace – 会被加在实体名字和数据库表名之前作为前缀的一个命名空间的名字。命名空间只能包含拉丁字母,而且应当越短越好。比如,项目的名字是
sales_2
,命名空间可以用sales
或者sal
。 -
Root package − Java 类的根包名。这个可以之后再调整,但是那些在项目创建时就生成的类将来是不会自动修改的。
-
Platform version – 项目中需要采用的 CUBA 框架的版本。在项目编译的时候相应的平台的依赖包会从仓库自动下载。
-
-
-
点击 Finish。会在指定的
sales
目录创建新的空项目,Studio 的主工作界面也会打开。如果您是首次使用 Studio,Studio 启动时会下载以及连接 Gradle 后台程序。还有,在首次使用 CUBA 框架构建的时候,Studio 会自动下载平台的源码和相应的依赖库。因此,打开项目以及组装项目可能会费一些时间。在开始项目工作之前,需要等待 Studio 的同步和创建索引工作完成。
-
在本地 HyperSQL 上创建数据库:选择菜单项 CUBA > Create database。数据库的名字默认就是项目的命名空间。
-
选择 CUBA > Start application server。或者也可以通过工具栏的 Run Configuration 下拉列表来启动应用程序。在 CUBA 项目树的 Runs at… 部分的项目链接可以用来直接从 Studio 打开浏览器展示项目。
默认的用户名密码是
admin
/admin
。应用运行起来会自带两个主菜单项(Administration 和 Help),以及 CUBA 框架提供的安全子系统功能、管理子系统功能。
2.3. 创建实体
下面我们创建客户(Customer
)实体类。
-
选中 CUBA 项目树面板中的 Data Model 部分,右键点击该节点,然后点击 New > Entity。这时会出现 New CUBA Entity 对话框窗口。
-
在 Entity name 字段输入新实体类的名字 –
Customer
。 -
点击 OK。然后会在工作区出现实体设计的界面。
-
这里会自动生成实体名字和数据库表名字,分别填写在 Entity name 和 Table 字段。
-
Parent 字段就按照默认给的值
StandardEntity
就可以。 -
Inheritance 字段不需改动。
下一步,创建实体的属性。点击 Attributes 表格下面的 New 按钮。
-
会出现 Create attribute 窗口。在 Name 字段输入属性名称
name
,在 Attribute type 字段选择属性的数据类型,选择DATATYPE
,在 Type 字段选择String
。勾上 Mandatory 复选框。然后会在 Column 字段自动生成数据库表的列名。点击 Add 添加属性。
-
email
属性也按照相同的方式创建。但是对于该字段,我们需要添加验证。在创建该属性之后,点击在字段属性配置栏的 Validation 区域点击 Email - not set 链接。 -
在窗口中勾选 Enabled 复选框并且填写验证错误消息
Email address is not valid
然后点击 OK。
现在切换到 Text 标签页,这里包含了 Customer
类的源代码。
点击类名称,然后用 Studio 的检查功能(inspection)来为 Customer
实体指定实例名称。在可选的属性列表中选择 name
。然后会生成类注解: @NamePattern("%s|name")
。
现在 Customer 实体创建完了。
下面创建 Order
实体。
右键点击 CUBA 项目树中的 Data Model 部分,然后点击 New > Entity。输入 Entity name − Order
。实体需要包含如下属性:
-
Name −
customer
, Attribute type −ASSOCIATION
, Type −Customer
, Cardinality −MANY_TO_ONE
。 -
Name −
date
, Attribute type −DATATYPE
, Type −Date
。勾选 Mandatory。 -
Name −
amount
, Attribute type −DATATYPE
, Type −BigDecimal
。
2.4. 创建数据库表
通过主菜单的 CUBA > Generate Database Scripts 来创建数据库表。之后,会弹出 Database Scripts 页。
包含针对当前数据库状态的增量 DB 更新脚本会展示在 Updates 标签页:
生成的数据库初始化脚本在 Init Tables, Init Constraints, 和 Init Data 标签页。
点击 Save and close 按钮保存生成的脚本。
如果要执行更新脚本,点击 CUBA > Update database 即可。也许需要先停止应用程序服务才能进行该操作。
2.5. 创建用户界面
现在开始创建 customer 和 order 管理界面。
2.5.1. Customer 界面
在 CUBA 项目树的 Data Model 部分右键点击 Customer
实体,选择 New > Screen 来创建查看和编辑 Customer
的标准界面。然后,Studio 会打开模板浏览界面。
在可用模板列表里选择 Entity browser and editor screens,然后点击 Next。
这个窗口的所有字段都已经填上了默认值,不需要修改了。点击 Next,下个界面中,本地化消息内容保持不变,然后点击 Finish。
界面文件会显示在 Generic UI 部分的 Screens 部分:
-
customer-browse.xml
- 浏览界面描述文件 -
CustomerBrowse
- 浏览界面控制器 -
customer-edit.xml
- 编辑界面描述文件 -
CustomerEdit
- 编辑界面控制器
2.5.2. Order 界面
Order
实体有一点不同的地方,由于 Order.customer
属性是引用属性,需要定义一个视图包含这个属性(因为标准的 _local
视图不包含引用属性)。
在 CUBA 项目树的 Data Model 部分右键点击 Order
实体,在菜单中点击 New > View。然后在视图设计界面,输入 order-with-customer
作为视图的名称,在右边的面板点击 customer
属性然后选择 Customer
实体的 _minimal
视图。
点击 OK。
之后,选择 Order
然后在右键菜单点击 New > Screen。
选择 Entity browser and editor screens 模板。
在 View 字段为浏览和编辑模板界面选择 order-with-customer
作为视图,然后点击 Next,下个界面点击 Finish。
界面文件会显示在 Generic UI 部分的 Screens :
-
order-browse.xml
- 浏览界面描述文件 -
OrderBrowse
- 浏览界面控制器 -
order-edit.xml
- 编辑界面描述文件 -
OrderEdit
- 编辑界面控制器
2.5.3. 应用程序菜单
在创建界面的同时,这些界面会被自动添加到默认的应用程序菜单的 application 菜单项中。现在重命名这个菜单,在 Studio 的 CUBA 项目树中切换到 Generic UI 部分,双击 Web Menu,这时会打开 web-menu.xml
描述文件。
将菜单标识符从 application-sales
改为 shop
,可以从代码直接改,也可以通过 Structure 标签页的可视化编辑器修改,效果一样。
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<menu-config xmlns="http://schemas.haulmont.com/cuba/menu.xsd">
<menu id="shop" insertBefore="administration">
<item screen="sales_Customer.browse"/>
<item screen="sales_Order.browse"/>
</menu>
</menu-config>
然后,在 Main Message Pack 部分打开 messages.properties
文件,修改菜单项的标题:
menu-config.shop = Shop
2.5.4. 带有 Order 列表的自定义编辑界面
按照下面的步骤在 Customer
编辑界面展示 orders 列表:
在 CUBA 项目树切换到 Generic UI 部分。打开 customer-edit.xml
进行编辑。
切换到 Designer 标签页。
在工具箱(palette)的数据组件分组中找到 Collection。将这个组件拖拽至组件树面板的 data 部分。
为数据容器选择 com.company.sales.entity.Order
实体及其 _local
视图。用 按钮生成加载器 ID。
在生成的查询语句中添加 WHERE 从句,选取关联到编辑的用户的 orders:
select e from sales_Order e where e.customer = :customer
最后,会得到一个在 customer-edit 界面 XML 描述中加载 Order
实例的数据容器:
<data>
<instance id="customerDc" class="com.company.sales.entity.Customer" view="_local">
<loader/>
</instance>
<collection id="ordersDc" class="com.company.sales.entity.Order" view="_local">
<loader id="ordersDl">
<query><![CDATA[select e from sales_Order e where e.customer = :customer]]></query>
</loader>
</collection>
</data>
从工具箱将 Table
组件拖至组件树面板,放置在 label
和 editActions
中间。在组件结构(Hierarchy)面板选中此组件,然后在 Properties 标签页设定表格的大小:在 width 字段设置 300px
,在 height 字段设置 200px
。从可用的数据容器中选择 ordersDc
。然后使用 id 字段边上的 按钮来生成表格的标识符:ordersTable
。
接下来,右键点击表格并选择 Wrap Into > Group Box。切换到 group box 属性面板的 Properties 标签页然后在 caption 字段输入 Orders
并设置 group box的宽度为 320 px。
如果应用程序需要使用多国语言,用 caption 字段旁边的 按钮来创建新的语言消息 |
最后,Text 标签页的 customer-edit.xml
代码如下:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd" caption="msg://editorCaption" focusComponent="form"
messagesPack="com.company.sales.web.customer">
<data>
<instance id="customerDc" class="com.company.sales.entity.Customer" view="_local">
<loader/>
</instance>
<collection id="ordersDc" class="com.company.sales.entity.Order" view="_local">
<loader id="ordersDl">
<query><![CDATA[select e from sales_Order e where e.customer = :customer]]></query>
</loader>
</collection>
</data>
<dialogMode height="600" width="800"/>
<layout expand="editActions" spacing="true">
<form id="form" dataContainer="customerDc">
<column width="250px">
<textField id="nameField" property="name"/>
<textField id="emailField" property="email"/>
</column>
</form>
<groupBox caption="Orders">
<table id="ordersDcTable" dataContainer="ordersDc" height="200px" width="300px">
<columns>
<column id="date"/>
<column id="amount"/>
</columns>
</table>
</groupBox>
<hbox id="editActions" spacing="true">
<button action="windowCommitAndClose"/>
<button action="windowClose"/>
</hbox>
</layout>
</window>
打开 CustomerEdit
界面控制器。左边栏的 和 按钮用来在界面描述器和控制器之间快速切换。
首先,我们需要禁止界面自动加载数据,因为我们需要自定义数据加载的过程。从类上删除 @LoadDataBeforeShow
就可以禁止自动加载数据了。
在控制器类中注入 orders 数据加载器,可以在类定义中按下 Alt+Insert
然后选择 ordersDl
。或者可以手动输入以下代码:
@Inject
private CollectionLoader<Order> ordersDl;
然后,订阅 BeforeLoadDataEvent
事件,为 ordersDl
数据加载器设置 customer
参数:按下 Alt+Insert 然后在 Generate 菜单中选择 Subscribe to Event > BeforeShowEvent。或者,也可以直接使用下面的代码:
@UiController("sales_Customer.edit")
@UiDescriptor("customer-edit.xml")
@EditedEntityContainer("customerDc")
public class CustomerEdit extends StandardEditor<Customer> {
@Inject
private CollectionLoader<Order> ordersDl;
@Subscribe
protected void onBeforeShow(BeforeShowEvent event) {
ordersDl.setParameter("customer", getEditedEntity());
getScreenData().loadAll();
}
}
这个方法将负责加载关联的 Order
实例。
2.6. 运行应用程序
现在我们看看真实的应用程序中刚才创建的界面都是什么样的。选择 CUBA > Start application server 或者直接点击 IDEA 工具栏的 "Run" 按钮。
在登录窗口用默认的用户和密码登录。打开 Shop > Customers 菜单项:
点击 Create 创建一个新 customer:
打开 Shop > Orders 菜单项:
点击 Create 创建一个新 order,在 Customer 字段选择新创建的 customer:
现在在 customer 编辑界面显示新的 order:
3. 框架详细介绍
本章节包含关于平台架构、通用组件以及工作机制的详细介绍。
3.1. 架构
本节从不同的角度介绍 CUBA 应用程序的体系结构,将按层(tier)、块(block)、模块(module)和组件(component)来介绍。
3.1.1. 应用程序层和块
框架支持使用多种客户端、中间层、数据库层构建多层应用程序。后续将主要介绍中间层和客户端层,因此,后面看到的“所有层”仅指这两个层。
应用程序的每个层都可以创建一个或多个应用程序 块(block)。每一个块都是独立可执行程序,可与应用程序中的其它块(block)交互。通常,一个块就是在 JVM 上运行的一个 web 应用程序。
- Middleware - 中间件
-
中间件层包含应用程序的核心业务逻辑,并提供对数据库的访问功能。由运行在 Java servlet 容器上的单独 Web 应用程序表示。参阅中间件组件。
- Web Client - Web 客户端
-
是客户端层的主要块(block)。包含主要为内部用户(即管理系统用户)设计的界面。由运行在 Java servlet 容器上的单独 Web 应用程序表示。用户界面基于 Vaadin 框架实现。参阅通用用户界面(GUI)。
- Web Portal - Web 门户
-
客户端层的附加块(block)。可以包含外部用户的界面以及作为与移动设备和第三方应用程序集成的入口点。由运行在 Java servlet 容器上的单独 Web 应用程序表示。用户界面基于 Spring MVC 框架实现。参阅Portal 组件。
- Frontend UI - 前端 UI
-
为外部用户设计的可选客户端,使用纯 JavaScript 编写。基于 Google Polymer 或者 React 框架实现,并通过在 Web Client 或 web Portal 块中运行的 REST API 与中间件进行通信。参阅 前端用户界面。
Middleware 是任何应用程序都需要的 block。用户界面可以由一个或多个 block 实现,例如 Web Client 和 Web Portal。
所有基于 Java 的客户端 block 都通过 HTTP 协议统一与中间层交互,从而可以任意部署中间层,即使在防火墙内也可以。在最简单的情况下,当中间层和 Web 客户端部署在同一服务器上时,它们之间的本地交互可以绕过网络栈以获得更好的性能。
3.1.2. 应用程序模块
模块是 CUBA 应用程序的最小组成部分。是应用程序项目中的单个模块,并具有相应的包含可执行代码的 JAR 文件。
标准模块:
-
global – 包含所有层共享的实体类、服务接口和其它类。用于所有应用程序 block。
-
core – 实现中间层的服务和所有其它组件。
-
gui – 包含通用用户界面的组件。用于 Web Client block。
-
web – 基于 Vaadin 的通用用户界面实现。
-
portal – 可选模块 - 基于 Spring MVC 的 Web 门户的实现。
-
front – 可选模块 - 使用 JavaScript 实现的前端用户界面。
3.1.3. 应用程序组件
框架支持将应用程序功能分成不同的组件。每个 应用程序组件(又名 扩展插件 )都可以有自己的数据模型、业务逻辑以及用户界面。应用程序将组件作为类库来使用并且包含了组件的所有功能。
应用程序组件的概念可以使得基础框架保持在一个比较小的范围,同时通过组件来交付一些可选功能,比如报表功能、全文检索功能、图表功能、WebDAV 以及其它功能。另外,应用程序开发人员也可以用这个机制来对大项目进行解耦,拆分成一组功能模块,从而每个模块可以有单独的开发计划和不同的发布周期。当然,应用程序组件是可重用的,并且可以在基础框架之上提供针对特定领域的抽象层。
技术上来说,核心框架也是一个名为 cuba 的应用程序组件。唯一不同的是,这个组件对于任何应用程序来说都是必不可少的。所有其它的组件都依赖 cuba,也可以互相依赖。
下面的图展示了应用中使用的标准组件之间的依赖关系。实线表示强制依赖,虚线表示可选依赖。
下面的图展示了标准组件和自定义应用程序组件之间可能的依赖结构。
任何 CUBA 应用程序都可以很容易的变成一个组件,从而为其它应用程序提供一些功能。要作为组件使用,应用程序项目应包含 app-component.xml 文件以及在 global 模块 JAR 的 manifest 中配置特殊的条目。CUBA Studio 可以为当前项目自动生成 XML 文件和 manifest 条目。
请参阅应用程序组件示例部分中使用自定义应用程序组件的分步介绍。
3.1.4. 应用程序结构
上面列出的架构原则在组装完成的应用程序结构能直接反映出来。假设我们有个简单的应用程序,包含两个 block - Middleware 和 Web Client ;并依赖了两个应用程序组件的功能 - cuba 和 reports。
该图展示了 Tomcat 服务的几个目录的内容,其中包含已部署的应用程序。
Middleware block 由 app-core
WEB 应用程序表示,Web Client block 由 app
Web 应用程序表示。Web 应用程序包含 WEB-INF/lib
目录下的 JAR 文件。每个 JAR(工件)都是一个应用程序模块或组件构建的结果。
比如,中间层 Web 应用程序 app-core
包含哪些 JAR 文件是由以下情况决定的:Middleware block 包含 global 和 core 模块,同时应用程序依赖了 cuba 和 reports 组件。
3.2. 通用组件
本章介绍平台组件,这些组件对应用程序的所有层都是通用的。
3.2.1. 数据模型
数据模型实体分为两类:
-
持久化实体 - 使用 ORM 将此类实体的实例存储在数据库中。
-
非持久化实体 – 实例仅存在于内存中,或通过不同的机制存储在某处。
实体通过其属性描述。属性对应到实体代码的字段以及字段的访问方法(get / set)。如果省略 setter,则该属性变为只读。
持久化实体可能包含未存储在数据库中的属性。对于非持久化属性,Java 字段不是必须的,可以只创建访问方法。
实体类应满足以下要求:
以下类型可用于实体属性:
-
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
基础实体类(见下文)重写 equals()
和 hashCode()
方法,这样可以通过比较它们的标识符来检查实体实例是否相同。即,如果它们的标识符相等,则认为他们是同一个实例。
3.2.1.1. 基础实体类
本节将详细介绍基础实体类和接口。
-
Instance
– 定义了使用应用程序领域对象的基本方法:-
获取对象元类的引用;
-
生成实例名称;
-
根据名称读写属性值;
-
添加监听器用于接收有关属性更改的通知。
-
-
Entity
– 继承自Instance
,添加了实体标识符;同时Entity
没有定义标识符的类型,该类型由其继承者决定。 -
AbstractInstance
– 实现了使用属性更改监听器的逻辑。 -
BaseGenericIdEntity
– 持久化和非持久化实体的基础类。实现了Entity
接口但没有指定实体标识符(即主键)的类型。 -
EmbeddableEntity
- 可嵌入的持久化实体的基础类。
下面是关于在项目实体中继承基础实体的一些建议。非持久化实体应该继承与持久化实体相同的基类。框架是根据实体所在注册文件:persistence.xml 或 metadata.xml 来确定实体是否是持久化实体。
- StandardEntity
-
继承自
StandardEntity
的实体带有一组标准功能:UUID 类型的主键、包含创建人和修改人及创建时间和修改时间、还支持乐观锁和软删除机制。
- BaseUuidEntity
-
继承自
BaseUuidEntity
的实体带有 UUID 类型主键,但不具备StandardEntity
的所有功能。可以在具体实体类中有选择地实现一些接口,如Creatable
、Versioned
等。
- BaseLongIdEntity
-
继承自
BaseLongIdEntity
或BaseIntegerIdEntity
的实体具有Long
或Integer
类型的主键。可以在具体实体类中有选择地实现一些接口,Creatable
、Versioned
等。强烈建议实现HasUuid
,因为它可以提供一些优化,并可以确保实例在分布式环境中的唯一标识。
- BaseStringIdEntity
-
继承自
BaseStringIdEntity
的实体具有String
类型的主键。可以在具体实体类中有选择地实现一些接口,如Creatable
、Versioned
等。强烈建议实现HasUuid
,因为它可以提供一些优化,并可以确保实例在分布式环境中的唯一标识。具体实体类必须有一个使用@Id
JPA 注解的字符串字段用来作为实体的主键。
- BaseIdentityIdEntity
-
继承自
BaseIdentityIdEntity
的实体,会映射到具有 IDENTITY 主键的表。可以在具体实体类中有选择地实现一些接口,如Creatable
、Versioned
等。强烈建议实现HasUuid
,因为它可以实现一些优化,并可以确保实例在分布式环境中的唯一标识。实体的id
属性(即getId()
/setId()
)是IdProxy
类型,用来替换真实标识符,真实标识符会在插入数据时由数据库生成。
- BaseIntIdentityIdEntity
-
继承
BaseIntIdentityIdEntity
的实体,会映射到Integer
类型的 IDENTITY 为主键的表(区别于BaseIdentityIdEntity
中的Long
)。在其它方面,BaseIntIdentityIdEntity
与BaseIdentityIdEntity
类似。
- BaseGenericIdEntity
-
除了上面情况之外,如果需要将实体映射到具有复合主键的表,则直接继承
BaseGenericIdEntity
。在这种情况下,具体实体类必须有一个嵌入类型的字段代表复合主键,并使用@EmbeddedId
JPA 注解。
3.2.1.2. 实体注解
本节介绍平台支持的实体类和属性的所有注解。
在本手册中,如果注解的标识是一个简单的类名,那么指的是 com.haulmont.*
包中的框架类之一。
3.2.1.2.1. 类注解
- @Embeddable
-
定义一个与所属实体存储在同一表中的嵌入实体。
应使用 @MetaClass 注解来指定实体名称。
- @EnableRestore
-
表示软删除的实体实例是否可以通过 Administration > Data Recovery 菜单打开的
core$Entity.restore
界面进行恢复。
- @Entity
-
声明一个类是一个数据模型实体。
参数:
-
name
– 实体的名称,必须带有前缀,以$
符号分隔前缀。建议使用项目的简称作为前缀来形成单独的命名空间。
例如:
@Entity(name = "sales$Customer")
-
- @Extends
-
表示该实体是一个对基础实体的扩展,在应用程序中应该使用它来代替其基础实体。参阅 功能扩展。
- @DiscriminatorColumn
-
在
SINGLE_TABLE
和JOINED
继承策略的情况下,用于定义负责区分实体类型的数据库列。参数:
-
name
– 鉴别器列名。 -
discriminatorType
– 鉴别器列的类型。
例如:
@DiscriminatorColumn(name = "TYPE", discriminatorType = DiscriminatorType.INTEGER)
-
- @DiscriminatorValue
-
定义此实体的鉴别器列值。
例如:
@DiscriminatorValue("0")
- @IdSequence
-
如果实体是
BaseLongIdEntity
或BaseIntegerIdEntity
的子类,则应明确定义用于生成标识符的数据库序列名称。如果实体没有此注解,则框架将自动生成名称并创建一个序列。参数:
-
name
– 序列名称。 -
cached
- 可选参数,定义序列应该以 cuba.numberIdCacheSize 递增,并将未使用的 ID 值缓存在内存中。默认为 False。
-
- @Inheritance
-
定义实体类的继承策略。此注解在实体继承层次的根类上指定。
参数:
-
strategy
– 继承策略,默认为SINGLE_TABLE
。
-
- @Listeners
- @MappedSuperclass
-
表示该类用作其它实体类的父类,其属性必须用作后代实体的一部分。这种类不关联任何特定的数据库表。
- @MetaClass
-
用于声明非持久化或嵌入实体(也就是不能用
@javax.persistence.Entity
注解)参数:
-
name
– 实体名称,必须以一个前缀开头,以$
符号分隔前缀。建议使用项目的简称作为前缀来形成单独的命名空间。
例如:
@MetaClass(name = "sales$Customer")
-
- @NamePattern
-
定义如何创建表示单一实体的字符串。可以认为是应用程序级别的
toString()
方法。在 UI 中到处都用得上,比如在类似TextField
或者LookupField
的单一字段中需要展示一个实体。也可以通过编程的方式使用MetadataTools.getInstanceName()
方法获取实例名称。注解值应该是
{0}|{1}
格式的字符串,其中:例如:
@NamePattern("%s|name")
@NamePattern("%s - %s|name,date")
@NamePattern("#getCaption|login,name")
- @PostConstruct
-
可以为方法指定此注解。在 Metadata.create() 方法创建实体实例之后将立即调用此方法。当实例初始化需要调用托管 Bean 时非常方便。请参阅 初始化实体字段。
- @PublishEntityChangedEvents
-
表示实体在数据库改动时,框架会发送 EntityChangedEvent 事件。
- @Table
-
定义实体的数据库表。
参数:
-
name
– 表名
例如:
@Table(name = "SALES_CUSTOMER")
-
- @TrackEditScreenHistory
-
表示系统将会保存编辑界面的打开历史记录,并能够通过 Help > History 主菜单项在
sec$ScreenHistory.browse
界面上显示。
3.2.1.2.2. 属性注解
应该为相应的字段设置属性注解,此情况除外:如果需要声明只读且非持久属性 foo
,则只创建 getFoo()
方法并使用 @MetaProperty
注解就可以了。
- @CaseConversion
-
表明使用此注解的实体属性绑定的文本输入框控件会自动转换大小写。
参数:
-
type
- 转换类型:UPPER
(默认值)、LOWER
。
示例:
@CaseConversion(type = ConversionType.UPPER) @Column(name = "COUNTRY_CODE") protected String countryCode;
-
- @Column
-
定义用于存储属性值的 DB 列。
参数:
-
name
– 列名。 -
length
– (可选参数,默认为255
) - 列字段的长度。还用于生成元数据,可以限制绑定到此属性的可视化组件中输入文本的最大长度。添加@Lob
注解可以移除对属性长度的限制。 -
nullable
– (可选参数,默认为true
) - 确定属性是否可以包含null
值。当nullable = false
时,JPA 会确保该字段在保存时有值。此外,带有该属性的可视化组件会要求用户必须输入值。
-
- @Composition
-
表示一种组合的关系,是比关联更紧密的一种关系。本质上,这意味着相关实体仅作为已有实体的一部分存在,也就是与已有实体一起创建和删除。
例如,订单中的条目列表(
Order
类包含Item
实例的集合,Item
实例不能独立存在):@OneToMany(mappedBy = "order") @Composition protected List<Item> items;
另一个例子是一对一的关系:
@Composition @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "DETAILS_ID") protected CustomerDetails details;
- @Embedded
-
定义可嵌入类型的引用属性。被引用实体应该具有
@Embeddable
注解。例如:
@Embedded protected Address address;
- @EmbeddedParameters
-
默认情况下,如果嵌入实体的所有属性在数据库中为空,则 ORM 不会创建嵌入实体的实例。当实例始终为非空时,可以使用
@EmbeddedParameters
注解指定不同的行为,例如:@Embedded @EmbeddedParameters(nullAllowed = false) protected Address address;
- @Id
-
表示该属性是实体主键。通常,此注解在基类的字段上设置,例如 BaseUuidEntity。仅在继承
BaseStringIdEntity
基类(即用于创建主键是字符串类型的实体)的情况下,才需要对特定实体类使用此注解。
- @IgnoreUserTimeZone
-
使框架忽略时间戳类型属性的用户时区(如果当前会话设置了时区),时间戳属性使用
@javax.persistence.Temporal.TIMESTAMP
注解。
- @JoinColumn
-
定义确定实体之间关系的 DB 列。存在此注解则表示该关联关系的拥有方(owning side)。
参数:
-
name
– 列名
例如:
@ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "CUSTOMER_ID") protected Customer customer;
-
- @JoinTable
-
定义
@ManyToMany
关系拥有方的关联表。参数:
-
name
– 关联表表名 -
joinColumns
– 关联表中的@JoinColumn
元素对应于关联关系拥有方的主键(包含@JoinTable
注解的那个) -
inverseJoinColumns
– 关联表中的@JoinColumn
元素对应于关系非拥有方(non-owning side)的主键。
关系拥有方的
Group
类的customers
属性示例:@ManyToMany @JoinTable(name = "SALES_CUSTOMER_GROUP_LINK", joinColumns = @JoinColumn(name = "GROUP_ID"), inverseJoinColumns = @JoinColumn(name = "CUSTOMER_ID")) protected Set<Customer> customers;
同一关系非拥有方的
Customer
类的groups
属性的示例:@ManyToMany(mappedBy = "customers") protected Set<Group> groups;
-
- @Lob
-
表示该属性没有任何长度限制。此注解与
@Column
注解一起使用。如果设置了@Lob
,则忽略@Column
中的默认或明确定义的长度。示例:
@Column(name = "DESCRIPTION") @Lob private String description;
- @LocalizedValue
-
确定获取属性的本地化值的方法,实现是使用 MessageTools.
getLocValue()
方法获取本地化值。参数:
-
messagePack
– 显式定义从哪个包中获取本地化消息的包名,例如,com.haulmont.cuba.core.entity
。 -
messagePackExpr
– 定义包名路径的表达式,包含本地化消息的包名称(例如,proc.messagesPack
)。路径从当前实体的属性开始。
下面示例中的注解表明
state
属性值的本地化消息应该从proc
实体的messagesPack
属性中定义的包中获取。@Column(name = "STATE") @LocalizedValue(messagePackExpr = "proc.messagesPack") protected String state; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "PROC_ID") protected Proc proc;
-
- @Lookup
-
定义引用属性的查找类型设置。
参数:
@Lookup(type = LookupType.DROPDOWN, actions = {"open"}) @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "CUSTOMER_ID") protected Customer customer;
- @ManyToMany
-
定义具有多对多关系类型的集合属性。
多对多关系可以有一个拥有方和一个反向的非拥有方。拥有方应使用
@JoinTable
注解,非拥有方则使用mappedBy
参数。参数:
不推荐使用
cascade
注解属性。使用此注解会隐式的对实体进行持久化和合并,这将绕过某些系统机制。特别是,EntityStates bean 将不能正确地检测托管状态,并且根本不会调用实体监听器。
- @ManyToOne
-
定义具有多对一关系类型的引用属性。
参数:
例如,几个
Order
实例引用相同的Customer
实例。在这种情况下,Order.customer
属性应该具有以下注解:@ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "CUSTOMER_ID") protected Customer customer;
不推荐使用
cascade
注解属性。使用此注解会隐式的对实体进行持久化和合并,这将绕过某些系统机制。特别是,EntityStates bean 将不能正确地检测托管状态,并且根本不会调用 实体监听器。
- @MetaProperty
-
表明元数据应包含带有此注解的属性。可以为字段设置此注解,但是如果没有字段,也可以为 getter 方法设置此注解。
已经带有
javax.persistence
包中的以下注解的字段不需要这个注解:@Column
、@OneToOne
、@OneToMany
、@ManyToOne
、@ManyToMany
、@Embedded
。这些字段自动包含在元数据中。因此,@MetaProperty
主要用于定义实体的非持久化属性。参数(可选):
字段示例:
@Transient @MetaProperty protected String token;
方法示例:
@MetaProperty public String getLocValue() { if (!StringUtils.isEmpty(messagesPack)) { return AppBeans.get(Messsages.class).getMessage(messagesPack, value); } else { return value; } }
- @NumberFormat
-
指定
Number
类型(BigDecimal
、Integer
、Long
或Double
)属性的格式。在所有的 UI 展示中,将按照注解参数提供的格式对属性值进行格式化和解析:-
pattern
- DecimalFormat 所描述的格式模板. -
decimalSeparator
- 用作小数位分隔符的字符(可选)。 -
groupingSeparator
- 用作千位分隔符的字符(可选)。
如果未指定
decimalSeparator
或groupingSeparator
,框架会使用当前用户的本地化格式字符串或服务器操作系统的本地化格式字符串。例如:
@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
-
在实体软删除的情况下,确定关联实体的处理策略。参阅软删除。
例如:
@OneToMany(mappedBy = "group") @OnDelete(DeletePolicy.CASCADE) private Set<Constraint> constraints;
- @OnDeleteInverse
-
从关系的反向软删除实体的情况下,确定属性关联实体的处理策略。参阅 软删除。
例如:
@ManyToOne @JoinColumn(name = "DRIVER_ID") @OnDeleteInverse(DeletePolicy.DENY) private Driver driver;
- @OneToMany
-
定义一对多关系类型的集合属性。
参数:
例如,几个
Item
实例使用@ManyToOne
注解的字段Item.order
引用相同的Order
实例。在这种情况下,Order
类可以包含Item
实例的集合:@OneToMany(mappedBy = "order") protected Set<Item> items;
不推荐使用 JPA
cascade
和orphanRemoval
注解属性。使用此注解会隐式的对实体进行持久化和合并,这将绕过某些系统机制。特别是,EntityStates bean 将不能正确地检测托管状态,并且根本不会调用 实体监听器。orphanRemoval
注解属性不遵循 软删除机制。
- @OneToOne
-
使用一对一关系类型定义引用属性。
参数:
Driver
类中关系的拥有方示例:@OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "CALLSIGN_ID") protected DriverCallsign callsign;
DriverCallsign
类中关系的非拥有方示例:@OneToOne(fetch = FetchType.LAZY, mappedBy = "callsign") protected Driver driver;
- @OrderBy
-
定义从数据库检索关联时集合属性中元素的顺序。需要对有序的 Java 集合(例如
List
或LinkedHashSet
)指定此注解,这样获得可预测的元素序列。参数:
-
value
– 确定排序的字符串格式:
orderby_list::= orderby_item [,orderby_item]* orderby_item::= property_or_field_name [ASC | DESC]
例如:
@OneToMany(mappedBy = "user") @OrderBy("createTs") protected List<UserRole> userRoles;
-
- @Temporal
-
指定
java.util.Date
类型属性的存储值类型:日期、时间或日期加时间。参数:
-
value
– 存储值的类型:DATE
、TIME
、TIMESTAMP
例如:
@Column(name = "START_DATE") @Temporal(TemporalType.DATE) protected Date startDate;
-
- @Transient
-
表示该字段不存储在数据库中,即属性是非持久化的。
字段的类型如果是 JPA 支持的(请参阅 Basic 注解类型),那么在默认情况下是持久化的,这就是为什么对于这些类型的非持久化属性必须要加上
@Transient
注解。如果元数据中需要包含
@Transient
属性,应该给属性加上 @MetaProperty 注解。
- @Version
-
表明注解的字段是用于支持乐观锁的版本字段。
当实体类实现
Versioned
接口时,需要这样的字段,StandardEntity
基类已经包含这样的字段。例如:
@Version @Column(name = "VERSION") private Integer version;
3.2.1.3. 枚举属性
JPA 对 enum
属性的标准用法是使用数据库的整型字段,保存从 ordinal()
方法获得的值。在生产环境下对系统进行扩展时,这种方法可能会导致以下问题:
-
如果数据库中枚举的值不等于任何
ordinal
值,则无法加载实体实例。 -
不能在现有的值之间添加新的枚举值,但是这在需要按枚举值排序时很重要。
CUBA 中解决这些问题的方式是将存储在数据库中的值与枚举的 ordinal
值分离。要到这一点,实体的字段应该用存储在数据库中的字段类型声明(Integer
或 String
型),而实体的访问方法(getter / setter)则使用实际的枚举类型来定义。
例如:
@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();
}
...
}
在这种情况下,枚举类可以如下所示:
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;
}
}
为了将枚举属性正常地反映在元数据中,枚举类必须实现 EnumClass
接口。
如示例所示,grade
属性对应于存储在数据库中的 Integer
类型值,该值由 CustomerGrade
枚举的 id
字段指定,即 10
、 20
或 30
。同时,应用程序代码和元数据框架通过访问方法(getter/setter)使用 CustomerGrade
枚举,这些方法中执行类型的转换。
如果数据库中的值没有对应的枚举值,这时 getGrade()
方法将只返回 null
。如果要添加一个新枚举值,例如 HIGHER
在 HIGH
和 PREMIUM
之间,只需添加 id = 15
的新枚举值就可以了,这样可以确保按 Customer.grade
字段正确排序。
Integer
字段类型可以提供有序的常量列表,并允许在 JPQL 和 SQL 查询中排序( >
、 <
、 >=
、 ⇐
、 order by
),同时也基本没有存储空间和性能方面的问题。但另一方面,在查询结果中,Integer
值不是“自描述”的,这使得在对数据库的原始数据或序列化后的数据进行调试时变地复杂了。就这点而言,使用 String
类型更方便。
可以在 CUBA Studio 中使用 Data Model > New > Enumeration 菜单创建枚举。要将枚举用作实体属性,请在属性编辑器的 Attribute type 字段中选择 ENUM
,然后在 Type 字段中选择枚举类。枚举值可以与在应用程序界面中显示的本地化名称相关联。
3.2.1.4. 软删除
CUBA 框架支持软删除模式,记录并不从数据库中物理删除,而是打上特定的标记,此时常规的方式将不能访问这些记录。后续可以使用某种定时任务将这些记录从数据库中完全删除或恢复。
软删除机制对于应用程序开发人员来说是透明的,唯一的要求是实体类实现 SoftDelete
接口。平台将自动调整数据操作。
软删除模式具有以下优点:
-
显著降低因不正确的用户操作而导致数据丢失的风险。
-
可以将某些记录标记为不可访问,即使系统中还存在对这些记录的引用。
使用 Orders-Customers 数据模型作为示例,假设某个客户已经下了多个订单,但我们需要禁止用户访问此客户实例。对于传统的硬删除来说,这是不可能的,因为删除客户实例需要删除所有相关订单或在订单中将所有对客户的引用设置为 null(意味着数据丢失)。软删除后,虽然客户实例无法进行搜索和修改,但是用户依然可以在订单编辑中看到客户的名称,因为在获取相关实体时有意忽略了删除属性。
上述标准行为可以使用关联实体处理策略进行修改。
已删除的实体实例可以在应用程序的 Administration 菜单中的 Restore Deleted Entities 界面上手动恢复。此功能仅适用于对所有实体具有所有操作权限的应用程序管理员,并且应该谨慎使用,因此建议拒绝让普通用户访问此界面。
软删除的负面影响是会增加数据库大小,以及可能需要额外的数据清理程序。
3.2.1.4.1. 软删除的使用
要支持软删除,实体类应该实现 SoftDelete
接口,相应的数据库表应该包含以下列:
-
DELETE_TS
– 删除记录的时间。 -
DELETED_BY
– 删除记录的用户登录名。
实现 SoftDelete
接口的实例的默认行为是,查询或按 id 搜索不返回软删除的实体。如果需要,可以使用以下方法动态关闭此特性:
-
当前 EntityManager 实例调用
setSoftDeletion(false)
。 -
通过 DataManager 请求数据时,
LoadContext
对象调用setSoftDeletion(false)
方法。 -
在数据源级别 - 在界面的 XML 描述中调用
CollectionDatasource.setSoftDeletion(false)
方法或设置collectionDatasource
元素的softDeletion="false"
属性。
在软删除模式下,平台在按标识符加载实例时、使用 JPQL 查询加载实例时以及在实体的集合属性中都会自动过滤掉被删除的实例。但是,在单一值(*ToOne)关联实体的时候却会加载相关联的实体,不管这个关联的实体是否已经被删除。
3.2.1.4.2. 关联实体处理策略
对于软删除实体,平台提供了一种在删除时管理关联实体的机制,很大程度上类似于数据库外键的 ON DELETE 规则。此机制在中间层有效,并且需要在实体属性上使用 @OnDelete,@OnDeleteInverse 注解。
使用 @OnDelete
注解的实体被删除时会处理此注解,而不是此注解指向的实体(这是与数据库级别级联删除的主要区别)。
@OnDeleteInverse
注解指向的实体被删除时处理此注解(类似于数据库中外键级别的级联删除)。当被删除的对象没有可以在删除之前检查的属性时,此注解很有用。典型情况下,被检查的对象具有要删除的对象引用,并且此属性应该使用 @OnDeleteInverse
注解。
注解值可以是:
-
DeletePolicy.DENY
– 如果带注解的属性不是null
或不是空集合,则禁止删除实体。 -
DeletePolicy.CASCADE
– 级联删除带注解的属性。 -
DeletePolicy.UNLINK
– 与注解属性断开链接。仅在关联关系拥有方(在实体类中具有@JoinColumn
注解的实体)断开链接是合理的。
例如:
-
禁止删除被引用的实体:如果尝试删除被至少一个
Order
引用的Customer
实例,则将抛出DeletePolicyException
。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;
异常窗口中的消息可以在主消息包中进行本地化。使用以下键值:
-
deletePolicy.caption
- 通知标题。 -
deletePolicy.references.message
- 通知消息。 -
deletePolicy.caption.sales$Customer
- 具体实体的通知标题。 -
deletePolicy.references.message.sales$Customer
- 具体实体的通知消息。
-
-
关联集合元素的级联删除:删除
Role
实例也会导致所有Permission
实例被删除。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;
-
断开与关联集合元素的连接:删除
Role
实例会导致对集合中包含的所有Permission
实例的Role
属性设置为空引用。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;
实现说明:
-
当保存实现了
SoftDelete
接口的实体到数据库时,会在中间件上处理关联实体策略。 -
将
@OnDeleteInverse
与CASCADE
和UNLINK
策略一起使用时要注意。在此过程中,将从数据库中提取关联对象的所有实例,进行修改然后保存。例如,如果
@OnDeleteInverse(CASCADE)
策略设置在Customer
–Job
关联内的Job.customer
属性上,代表一个 customer 有多个 job,删除 Customer 实例时将获取和修改所有 Job。这可能会导致应用程序服务器或数据库超负荷。另一方面,使用
@OnDeleteInverse(DENY)
是安全的,因为它只涉及统计关联对象的数量。如果数量大于0
,则抛出异常。这使得@OnDeleteInverse(DENY)
适用于Job.customer
属性。
3.2.1.4.3. 数据库级别的唯一约束
为了在软删除模式中对特定值应用唯一约束,也就是说在数据库中可以存在至少一个具有该值的未删除记录和任意数量的具有相同值的已删除记录。
此逻辑可以针对每种数据库服务类型以特定方式实现:
-
如果数据库服务支持部分索引(例如 PostgreSQL),则可以按如下方式实现唯一约束:
create unique index IDX_SEC_USER_UNIQ_LOGIN on SEC_USER (LOGIN_LC) where DELETE_TS is null
-
如果数据库服务不支持部分索引(例如 Microsoft SQL Server 2005),则 DELETE_TS 字段可以包含在唯一索引中:
create unique index IDX_SEC_USER_UNIQ_LOGIN on SEC_USER (LOGIN_LC, DELETE_TS)
3.2.2. 元数据框架
元数据框架用于在 CUBA 应用程序中高效处理数据模型。此框架:
-
提供 API 以获取有关实体、实体属性和实体之间关系的信息,还能用来遍历对象关系图;
-
作为 Java 反射 API 更专用和更方便的替代品;
-
控制允许的数据类型以及实体之间的关系;
-
允许实现通用的数据操作机制;
3.2.2.1. 元数据接口
以下是基本的元数据接口:
- Session
-
元数据框架的入口点。允许按名称或相应的 Java 类获取
MetaClass
实例。注意方法的不同:getClass()
方法可以返回null
而getClassNN()
(NotNull)方法不能。可以使用元数据基础接口获得
Session
对象。示例:
@Inject
protected Metadata metadata;
...
Session session = metadata.getSession();
MetaClass metaClass1 = session.getClassNN("sec$User");
MetaClass metaClass2 = session.getClassNN(User.class);
assert metaClass1 == metaClass2;
- MetaModel
-
很少使用的接口,用于对元类进行分组。
元类按照 metadata.xml 文件中指定的 Java 项目包的根名称进行分组。
- MetaClass
-
实体类元数据接口。
MetaClass
始终与它所代表的 Java 类相关联。基本方法:
-
getName()
– 实体名称,根据惯例,在$
符号之前名称的第一部分是项目命名空间代码,例如sales$Customer
。 -
getProperties()
– 元属性列表(MetaProperty
)。 -
getProperty()
、getPropertyNN()
– 按名称返回元属性。如果没有与提供的名称对应的属性,则第一个方法返回null
,第二个方法抛出异常。示例:
MetaClass userClass = session.getClassNN(User.class); MetaProperty groupProperty = userClass.getPropertyNN("group");
-
getPropertyPath()
– 允许通过引用进行属性遍历。此方法接收属性路径参数 - 以点分隔的属性名字符串。方法返回MetaPropertyPath
对象,可通过调用此对象的getMetaProperty()
方法访问所需的(路径中的最后一个)属性。示例:
MetaClass userClass = session.getClassNN(User.class); MetaProperty groupNameProp = userClass.getPropertyPath("group.name").getMetaProperty(); assert groupNameProp.getDomain().getName().equals("sec$Group");
-
getJavaClass()
– 对应于这个MetaClass
的实体类。 -
getAnnotations()
– 元注解集合。
-
- MetaProperty
-
实体属性元数据接口。
基本方法:
-
getName()
– 属性名,对应于实体属性名。 -
getDomain()
– 拥有这个属性的元类。
-
-
getType()
- 属性类型:-
简单类型:
DATATYPE
-
枚举:
ENUM
-
两种引用类型:
-
ASSOCIATION - 关联
− 简单引用另一个实体。例如,订单 - 客户的关系就是一种关联关系。 -
COMPOSITION - 组合
− 引用一个实体,这个实体在脱离拥有它的实体时会失去实际意义。COMPOSITION
被认为是一种比ASSOCIATION
更紧密的关系。例如,Order 和它的 Items 之间的关系是一个COMPOSITION
,因为如果没有 Item 所属的 Order,Item 就不能存在。引用属性的
ASSOCIATION
或COMPOSITION
类型影响实体编辑模式:在第一种情况下,相关实体独立地持久化到数据库,在第二种情况下,相关实体仅与父实体一起持久化。有关详细信息,请参阅组合结构。
-
-
-
getRange()
–Range
接口提供属性类型的详细描述。 -
isMandatory()
– 表示必须属性。例如,可视化组件以此来提示用户必须输入值。 -
isReadOnly()
– 表示只读属性。 -
getInverse()
– 对于引用类型的属性,返回关联另一侧的元属性(如果存在)。 -
getAnnotatedElement()
– 对应于实体属性的字段(java.lang.reflect.Field
)或方法(java.lang.reflect.Method
)。 -
getJavaType()
– 实体属性的 Java 类。可以是相应字段的类型,也可以是相应方法的返回值的类型。 -
getDeclaringClass()
– 包含这个属性的 Java 类。
- Range
-
详细描述实体属性类型的接口。
基本方法:
-
isDatatype()
– 如果是简单类型属性则返回true
。 -
asDatatype()
– 对于简单类型属性返回对应的Datatype实例。 -
isEnum()
– 如果是枚举类型返回true
。 -
asEnumeration()
– 对于枚举类型属性返回Enumeration实例。 -
isClass()
– 如果是ASSOCIATION
或COMPOSITION
类型属性,则返回true
。 -
asClass()
– 对于引用属性返回关联实体的元类。 -
isOrdered()
– 如果属性由有序集合(例如List
)表示,则返回true
。 -
getCardinality()
– 引用属性的关系类型:ONE_TO_ONE
、MANY_TO_ONE
、ONE_TO_MANY
、MANY_TO_MANY
。
-
3.2.2.2. 元数据构建
元数据结构生成的主要来源是带注解的实体类。
实体类满足以下情况将出现在元数据中:
-
带
@Entity
、@Embeddable
、@MappedSuperclass
注解并且位于 metadata.xml 中指定的根包中的持久化实体类。 -
带
@MetaClass
注解且位于metadata.xml
中指定的根包中的非持久化实体类。
同一根包中的所有实体都放在同一个 MetaModel
实例中,该实例被赋予该包的名称。同一个 MetaModel
中的实体可以包含任意彼此的引用。可以按 cuba.metadataConfig 属性中的 metadata.xml
文件的声明顺序创建来自不同元模型的实体之间的引用。
实体属性满足以下条件时将出现在元数据中:
-
使用
@Column
、@OneToOne
、@OneToMany
、@ManyToOne
、@ManyToMany
、@Embedded
注解的类字段。 -
使用
@MetaProperty
注解的类字段或字段访问方法(getter)。
Metaclass 和 metaproperty 参数是基于上面列出的注解参数以及字段类型和类方法确定的。但是,如果属性没有写设置方法(setter),那么该属性将是只读的。
3.2.2.3. 数据类型接口
Datatype
接口定义了值和字符串互转的方法(格式化和解析)。每个实体属性,如果不是引用属性的话,就会有一个对应的 Datatype
,平台用 Datatype
来格式化和解析属性值。
数据类型注册在 DatatypeRegistry
bean 中,这个 bean 根据项目以及项目依赖的应用程序组件中的 metadata.xml 文件对 Datatype
的实现类进行加载和初始化。
实体属性的数据类型可以通过相应的元属性使用 getRange().asDatatype()
方法获取。
也可以使用注册的数据类型来格式化或者解析支持的数据类型的任意属性值。如需这么做,可以从 DatatypeRegistry
通过调用 get(Class)
或者 getNN(Class)
方法得到数据类型实例,传入需要转换的 Java 类型即可。
数据类型通过下列规则跟实体属性关联:
-
大多数情况下,一个实体属性都关联一个注册的
Datatype
实例,用来处理属性的 Java 类型。下面的例子中,
amount
属性关联的是BigDecimalDatatype
@Column(name = "AMOUNT") private BigDecimal amount;
因为
com/haulmont/cuba/metadata.xml
有下面的记录:<datatype id="decimal" class="com.haulmont.chile.core.datatypes.impl.BigDecimalDatatype" default="true" format="0.####" decimalSeparator="." groupingSeparator=""/>
-
可以通过 @MetaProperty 注解的
datatype
属性来显式指定一个数据类型。下面的例子中,实体的
issueYear
属性将会关联year
数据类型。@MetaProperty(datatype = "year") @Column(name = "ISSUE_YEAR") private Integer issueYear;
如果项目的
metadata.xml
文件有下面的记录:<datatype id="year" class="com.company.sample.YearDatatype"/>
所以可以看到,
@MetaProperty
的datatype
属性包含标识符,这个标识符用来在metadata.xml
文件中注册数据类型的实现类。
datatype
接口的基本方法:
-
format()
– 将传入的值格式化成字符串。 -
parse()
– 将字符串转换成相应类型的值。 -
getJavaClass()
– 返回这个数据类型对应的 Java 类型。这个方法有一个默认实现,如果类带了@JavaClass
注解,这个实现会返回此注解的值。
Datatype
定义了用来做格式化和解析的两组方法:一组会考虑应用所在的 locale,另一组不会考虑。考虑了 locale 的转换用在用户界面的各个地方,不考虑 locale 的方法用在系统机制中,比如,在 REST API 的序列化时。
使用忽略 locale 的解析需要在代码中写死或者在 metadata.xml
文件注册数据类型时显式指定。
参考下一节了解怎么使用依赖 locale 的解析。
3.2.2.3.1. 数据类型格式化字符串
-
numberDecimalSeparator
– 数值类型的小数分隔符。 -
numberGroupingSeparator
– 数值类型的千分位符。 -
integerFormat
–Integer
和Long
类型的格式。 -
doubleFormat
–Double
类型的格式。 -
decimalFormat
–BigDecimal
类型的格式。 -
dateTimeFormat
–java.util.Date
类型的格式。 -
dateFormat
–java.sql.Date
类型的格式。 -
timeFormat
–java.sql.Time
类型的格式。 -
trueString
–Boolean.TRUE
类型对应的显示字符串。 -
falseString
–Boolean.FALSE
类型对应的显示字符串。
通过 Studio 可以设置针对应用程序使用的语言的格式化字符串。编辑 Project Properties,点击 Available locales 字段的编辑按钮,然后勾选 Show data format strings。 |
locale 相应的格式化字符串可以通过 FormatStringsRegistry
bean 获得。
3.2.2.3.2. 自定义数据类型示例
假设应用程序中实体的某些属性用来存储日期的年份,用整数数字表示。用户能查看和编辑年份,并且如果用户只是输入两个数字,应用程序会将其转换为 2000 至 2100 之间的一个年份,否则,将整个输入的数字作为年份。
首先,在 global 模块创建下面的类:
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);
}
}
然后在项目的 metadata.xml 文件中添加 datatypes
元素:
<metadata xmlns="http://schemas.haulmont.com/cuba/metadata.xsd">
<datatypes>
<datatype id="year" class="com.company.sample.entity.YearDatatype"/>
</datatypes>
<!-- ... -->
</metadata>
在 datatype
元素中,还可以指定 sqlType
属性来包含一个数据库的 SQL 类型,这个 SQL 类型适合在数据库保存这个新类型。SQL 类型会在 CUBA Studio 生成数据库脚本的时候使用。Studio 可以为下面这些 Java 类型自动确定 SQL 类型:
-
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[]
在上面的例子中,年份应该绑定 Integer
类型(这个类型通过 @JavaClass
注解的 Integer.class
值指定),所以 sqlType
可以省去。
最终,为实体属性设置新的数据类型(编程的方式或者通过 Studio 的帮助):
@MetaProperty(datatype = "year")
@Column(name = "ISSUE_YEAR")
private Integer issueYear;
3.2.2.3.3. 用户界面中数据格式化示例
这里以如何在 Orders 表格中显示 Order.date
属性为例。
order-browse.xml
<table id="ordersTable">
<columns>
<column id="date"/>
<!--...-->
Order
类的 date
属性是用 "date" 类型来定义的:
@Column(name = "DATE_", nullable = false)
@Temporal(TemporalType.DATE)
private Date date;
如果当前用户使用俄语 locale 登录,会从主语言消息包获取下面的字符串:
dateFormat=dd.MM.yyyy
结果是 "2012-08-06" 日期会被转换为 "06.08.2012" 字符串显示在表格的单元格中。
3.2.2.3.4. 代码中的日期和数字格式化示例
如果需要按照用户当前的 locale 格式化或者解析 BigDecimal
、 Integer
、 Long
、 Double
、 Boolean
或者 Date
类型,可以使用 DatatypeFormatter
bean,示例:
@Inject
private DatatypeFormatter formatter;
void sample() {
String dateStr = formatter.formatDate(dateField.getValue());
// ...
}
下面是直接使用 Datatype
的例子。
-
日期格式化示例:
@Inject protected UserSessionSource userSessionSource; @Inject protected DatatypeRegistry datatypes; void sample() { Date date; // ... String dateStr = datatypes.getNN(Date.class).format(date, userSessionSource.getLocale()); // ... }
-
在 Web 客户端格式化数字,显示最多 5 位小数示例:
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); // ... }
3.2.2.4. 元注解
实体元注解是一组提供有关实体的附加信息的键值对(key-value)。 使用元类的 getAnnotations()
方法访问元注解。 元注解的来源是:
-
@OnDelete
、@OnDeleteInverse
、@Extends
注解。通过这些注解将创建描述实体之间关系的特殊元注解。 -
可扩展的元注解用
@MetaAnnotation
标明。这些注解会转换成元注解,key 是注解对应的 Java 类的全名称,value 是一组注解属性的映射。比如,@TrackEditScreenHistory
注解的 value 就是一个单一的映射:value → true
。平台提供几个这种类型的注解:@NamePattern
,@SystemLevel
,@EnableRestore
,@TrackEditScreenHistory
。在应用程序或者应用程序组件中,可以创建自定义的注解类然后使用@MetaAnnotation
对自定义注解类进行标注。 -
可选:实体元注解也可以在metadata.xml文件中定义。如果 XML 中的元注解与 Java 实体类注解创建的元注解具有相同的名称,那么它将覆盖后者。
下面的示例演示了如何通过
metadata.xml
中的元注解覆盖 Java 类中的注解:<metadata xmlns="http://schemas.haulmont.com/cuba/metadata.xsd"> <!-- ... --> <annotations> <entity class="com.company.customers.entity.Customer"> <annotation name="com.haulmont.cuba.core.entity.annotation.TrackEditScreenHistory"> <attribute name="value" value="true" datatype="boolean"/> </annotation> <property name="name"> <annotation name="length" value="200"/> </property> <property name="customerGroup"> <annotation name="com.haulmont.cuba.core.entity.annotation.Lookup"> <attribute name="type" class="com.haulmont.cuba.core.entity.annotation.LookupType" value="DROPDOWN"/> <attribute name="actions" datatype="string"> <value>lookup</value> <value>open</value> </attribute> </annotation> </property> </entity> <entity class="com.company.customers.entity.CustomerGroup"> <annotation name="com.haulmont.cuba.core.entity.annotation.EnableRestore"> <attribute name="value" value="false" datatype="boolean"/> </annotation> </entity> </annotations> </metadata>
3.2.3. 视图
从数据库中检索实体时,我们经常面临一个问题:如何确保将关联实体加载到所需的深度?
例如,需要在 Order 浏览界面中显示日期、金额以及客户名称,这意味着需要获取相关的 Customer 实例。还有,在 Order 编辑界面,需要获取 Item 集合,此外每个 Item 应包含一个相关的 Product 实例以显示其名称。
延迟加载在大多数情况下都无济于事,因为数据处理通常不是在加载实体的事务中执行,而是在像 UI 这样的客户端层执行。同样,使用实体注解来应用贪婪加载也是不可行的,因为这样做会导致总是查询对象关系图中所有的关联实体,这可能是非常大的数据。
另一个类似的问题是需要限制加载对象图中的本地属性的集合:例如,某个实体可以有 50 个属性,包括 BLOB,但是只需要在界面上显示 10 个属性。此时,对于不需要的 40 个额外属性,为什么要从数据库加载然后将它们序列化并传输到客户端?
视图 机制通过从数据库中检索并向客户端传输有深度和属性限制的实体关系图来解决这些问题。特定 UI 界面或数据处理操作所需的对象图通过 视图 来描述。
视图处理过程按以下方式执行:
视图由 View
类的实例确定,其中:
-
entityClass
– 定义视图的实体类。换句话说,它是加载的实体树的“根”。 -
name
– 视图名称。可以是“null”或实体的所有视图中的唯一名称。 -
properties
– 对应需要加载的实体属性的ViewProperty
实例的集合。 -
includeSystemProperties
– 如果设置,则视图将包含系统属性(由持久化实体的基础接口定义,如BaseEntity
和Updatable
)。
-
loadPartialEntities
- 指定视图是否影响本地(立即加载)属性的加载。如果为 false,则仅影响引用属性,并且会始终加载本地属性,而无论它们是否存在于视图中。此属性在某种程度上由平台数据加载机制控制,请参阅有关在 DataManager 和 EntityManager 中加载部分实体的章节。
ViewProperty
类具有以下属性:
-
name
– 实体属性名。 -
view
– 对于引用属性,指定将用于加载关联实体的视图。 -
fetch
- 对于引用属性,指定如何从数据库中获取关联实体。其值对应于FetchMode
枚举类型:-
AUTO
- 平台将根据关系类型选择最佳模式。 -
UNDEFINED
- 将根据 JPA 规则执行提取,这实际上意味着通过单独的 select 进行加载。 -
JOIN
- 通过关联引用的表来在同一个 select 查询中获取。 -
BATCH
- 相关对象的查询将通过分批的方式进行优化。参阅:这里。
如果未指定
fetch
属性,则应用AUTO
模式。如果引用是一个可缓存的实体,则无论视图中指定的什么值,都将使用UNDEFINED
。 -
无论视图中的属性如何定义,始终会加载以下属性:
|
尝试获取或设置未加载属性的值(未包含在视图中)会引发异常。您可以使用 |
3.2.3.1. 创建视图
视图可以通过两种方式创建:
-
编程方式 – 通过创建
View
实例,例如:View view = new View(Order.class) .addProperty("date") .addProperty("amount") .addProperty("customer", new View(Customer.class) .addProperty("name") );
通常,这种方式适用于创建在单个业务逻辑中使用的视图。
-
声明式 – 通过创建 XML 描述并将其部署到
ViewRepository
。部署 XML 描述时,会创建并缓存View
实例。此外,通过ViewRepository
(调用时提供实体类和视图名称),可以在应用程序代码的任何部分中获取所需的视图。
下面看看使用声明式方式创建和使用视图的细节。
ViewRepository
是一个 Spring bean,所有应用程序 block 都可以访问这个 bean。可以使用注入或通过元数据基础接口得到 ViewRepository
的引用。然后用 ViewRepository.getView()
方法从视图库中获取视图实例。AbstractViewRepository
基实现中的 deployViews()
方法用于将 XML 描述部署到视图库。
默认情况下,每个实体的视图库中都有三个名为 _local
、 _minimal
和 _base
的视图:
-
_local
包含所有本地实体属性。 -
_minimal
包含实体实例名称中包含的属性,这些属性通过@NamePattern注解指定。如果未在实体中指定@NamePattern
注解,则此视图不包含任何属性。 -
_base
包括所有本地非系统属性和由@NamePattern
定义的属性(实际上是_minimal
+_local
)。
视图 XML 描述的详细结构说明在这里。
下面的示例展示了 Order
实体的视图描述,它提供了所有本地属性、关联的 Customer
和 Items
集合的加载。
<view class="com.sample.sales.entity.Order"
name="order-with-customer"
extends="_local">
<property name="customer" view="_minimal"/>
<property name="items" view="itemInOrder"/>
</view>
推荐按照下面方法进行视图描述分组和部署:
-
在 global 模块的
src
根目录中创建 views.xml文件,并将所有可全局(即在所有应用程序层)访问的视图描述放入其中。 -
在所有 block 的 cuba.viewsConfig应用程序属性中注册此文件,即 core 模块的
app.properties
、 web 模块的web-app.properties
,这将确保在应用程序启动时自动部署视图到视图库中。 -
如果视图仅在一个应用程序 block 中使用,则可以在此 block 的类似文件中定义,例如,
web-views.xml
,然后只在这个 block 的cuba.viewsConfig属性中注册。如果视图库已经包含某个实体的特定名称的视图,则不会为此实体部署其它同名视图。如果需要使用新的视图替换视图库中的现有视图并保证其部署,需要为新视图指定
overwrite="true"
属性。
建议为视图提供描述性名称。例如,不仅仅是 "browse",而是 "customerBrowse"。这样可以简化 XML 描述中视图的搜索。
3.2.4. 托管 Bean
Managed Beans - 托管 bean 是用于实现应用程序业务逻辑的程序组件。这里,Managed 意味着实例的创建和依赖关系的管理都由容器处理,容器是 Spring 框架的主要部分。
默认情况下,托管 Bean 是 单例,即每个应用程序 block 中只存在此类的一个实例。如果单例 bean 在字段中包含可变数据(换句话说,具有状态),对这些数据的访问必须同步。 |
3.2.4.1. 创建托管 Bean
如需创建托管 bean,只需要在 Java 类添加 @org.springframework.stereotype.Component
注解。示例:
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) {
}
}
建议为 bean 分配一个唯一的名称,按照 {project_name}_{class_name}
格式,并且用常量 NAME
定义。
|
托管 bean 的类定义需要放在 spring.xml 文件的 context:component-scan
元素指定的扫描目录树下。此时,spring.xml
的示例:
<context:component-scan base-package="com.sample.sales"/>
也就是说,扫描此应用程序 block 中带注解的 bean 会从包的 com.sample.sales
目录开始。
托管 bean 可以在任何层创建,因为 Spring 框架的容器在应用程序所有标准的 block 里面都有用到。
3.2.4.2. 使用托管 Bean
托管给 Spring 的 bean 可以通过注入或者通过 AppBeans
类获取。作为使用 bean 的示例,看看 OrderService
bean 的实现,它将具体的执行代理给了 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);
}
}
3.2.5. JMX Beans
有时,有必要让系统管理员能够在运行时查看和更改某些托管 bean 的状态。在这种情况下,建议创建一个 JMX bean - 一个具有 JMX 接口的程序组件。JMX bean 通常是一个包装器,代理了对托管 bean 的调用,但实际上还是由托管 bean 维护状态:比如缓存、配置数据或统计信息。
从图中可以看出,JMX bean 由接口和实现类组成。实现类应该是一个托管 bean,即应该具有 @Component
注解和唯一名称。JMX bean 的接口以一种特殊的方式在 spring.xml 中注册,以便在当前 JVM 中创建 JMX 接口。
使用 Spring AOP 通过 MBeanInterceptor
拦截器类拦截对所有 JMX bean 接口方法的调用,该类在当前线程中设置正确的 ClassLoader
并对未处理异常进行日志记录。
JMX bean 接口名称必须符合以下格式: |
JMX 接口可以由外部工具使用,例如 jconsole 或 jvisualvm。此外,平台的 Web 客户端提供了 JMX console 功能,可以通过这个功能查看托管 Bean 状态、调用 JMX bean 方法。
3.2.5.1. 创建 JMX Bean
以下示例展示如何创建 JMX Bean。
-
JMX bean 接口:
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); }
-
接口和其方法上可定义注解,通过这些注解来对 JMX Bean 和它提供的操作进行描述。这些描述会显示在使用 JMX 接口的所有工具中,这样可以帮助系统管理员了解接口。
-
可选的
@JmxRunAsync
注解用来标识出一个需要长时间执行的操作。当使用内置的 JMX console 运行此操作时,平台将会显示一个带有无限进度条和 Cancel 按钮的对话框。用户可以中断操作并继续使用应用程序。该注解还可以包含timeout
参数,该参数用来设置操作的最长执行时间(以毫秒为单位),例如:@JmxRunAsync(timeout = 30000) String calculateTotals();
如果执行超时,对话框将关闭并显示错误消息。
请注意,如果在用户界面上取消操作,或者观察到操作超时,但操作实际上继续在后台运行,也就是说这些操作不能实际终止,只是将控制权交给用户。
-
由于 JMX 工具只支持有限的数据类型,因此最好使用
String
作为方法的参数和结果类型,必要时,需要在方法内进行类型转换。除了String
外,还支持以下参数类型:boolean
、double
、float
、int
、long
、Boolean
、Integer
。
-
-
JMX bean 类:
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); } } }
@Component
注解将类定义为具有sales_OrdersMBean
名称的托管 bean,该名称直接在注解中指定而不是在常量中,因为不需要从 Java 代码访问 JMX bean。下面概述一下
calculateTotals()
方法的实现。 -
在
spring.xml
中注册 JMX bean:<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>
项目的所有 JMX beans 都是在
beans
属性的map/entry
元素中的一个MBeanExporter
实例中声明的。键是 JMX ObjectName,值是在@Component
注解中指定的 bean 名称。ObjectName 以 web 应用程序的名称开始,因为可以将输出相同 JMX 接口的多个 web 应用程序部署到一个应用程序服务实例中(部署到 JVM 中)。
3.2.5.2. 平台内置 JMX Beans
本节介绍平台中可用的一些 JMX beans。
3.2.5.2.1. CachingFacadeMBean
JMX 对象名: app-core.cuba:type=CachingFacade
和 app.cuba:type=CachingFacade
3.2.5.2.2. ConfigStorageMBean
该接口具有独立的一组操作,用于处理存储在文件 (*AppProperties
) 中和存储在数据库 (*DbProperties
) 中的属性。这些操作只显示在对应的存储中明确设置了值的属性。也就是说如果配置接口定义了属性和其默认值,但未在数据库(或文件)中设置该值,则这些方法将不显示该属性及其当前值。
请注意,对存储在文件中的属性值的修改不是持久化的,并且只有在重启应用程序模块后才能生效。
与上述操作不同,getConfigValue()
操作返回的值与在应用程序代码中调用配置接口的相应方法返回的值完全相同。
JMX 对象名:
-
app-core.cuba:type=ConfigStorage
-
app.cuba:type=ConfigStorage
-
app-portal.cuba:type=ConfigStorage
3.2.5.2.4. PersistenceManagerMBean
PersistenceManagerMBean 提供以下功能:
-
管理实体统计信息机制。
-
使用
findUpdateDatabaseScripts()
方法查看新的数据库更新脚本。使用updateDatabase()
方法触发数据库更新。 -
使用
jpqlLoadList()
、jpqlExecuteUpdate()
方法可以在中间层上下文中执行任意的 JPQL 查询
JMX 对象名: app-core.cuba:type=PersistenceManager
3.2.5.2.5. ScriptingManagerMBean
ScriptingManagerMBean 是 Scripting 基础接口的 JMX 外观设计模式实现。
JMX 对象名: app-core.cuba:type=ScriptingManager
JMX 属性:
JMX 操作:
-
runGroovyScript()
– 在中间层上下文中执行 Groovy 脚本并返回结果。会将以下变量传递给脚本:-
persistence
- Persistence 类型的参数。 -
metadata
- Metadata 类型的参数。 -
configuration
- Configuration 类型的参数。 -
dataManager
- DataManager 类型的参数。结果类型应该是可显示 JMX 界面上的字符串类型。除此之外,此方法与 Scripting.runGroovyScript() 完全一样。
用于创建一组测试用户的示例脚本,如下所示:
import com.haulmont.cuba.core.* import com.haulmont.cuba.core.global.* import com.haulmont.cuba.security.entity.* PasswordEncryption passwordEncryption = AppBeans.get(PasswordEncryption.class) Transaction tx = persistence.createTransaction() try { EntityManager em = persistence.getEntityManager() Group group = em.getReference(Group.class, UUID.fromString('0fa2b1a5-1d68-4d69-9fbd-dff348347f93')) for (i in (1..250)) { User user = new User() user.setGroup(group) user.setLogin("user_${i.toString().padLeft(3, '0')}") user.setName(user.login) user.setPassword(passwordEncryption.getPasswordHash(user.id, '1')); em.persist(user) } tx.commit() } finally { tx.end() }
-
3.2.5.2.6. ServerInfoMBean
ServerInfoMBean 提供了有关此中间层模块的通用信息:内部版本号、 构建日期 和 服务器 ID.
JMX 对象名: app-core.cuba:type=ServerInfo
3.2.6. 基础设施接口
基础设施接口提供对平台的常用功能的访问。它们中的大多数位于 global 模块中,并且可以在中间层和客户端层使用。但是,其中一些(例如 Persistence )只能用于中间层代码。
此外,与任何其它 bean 一样,基础接口可以通过 AppBeans
类的静态方法获取,然后可以在非托管组件(POJO、helper 类等)中使用。
3.2.6.1. Configuration
该接口有助于获取配置接口的引用。
例如:
// field injection
@Inject
protected Configuration configuration;
...
String tempDir = configuration.getConfig(GlobalConfig.class).getTempDir();
// setter injection
protected GlobalConfig globalConfig;
@Inject
public void setConfiguration(Configuration configuration) {
this.globalConfig = configuration.getConfig(GlobalConfig.class);
}
// location
String tempDir = AppBeans.get(Configuration.class).getConfig(GlobalConfig.class).getTempDir();
3.2.6.2. DataManager
DataManager
接口在中间层和客户端层提供 CRUD 功能,是一种通用工具,用于从数据库加载实体关系图并保存已更改的游离实体实例。
有关 DataManager 与 EntityManager 之间差异的信息,请参阅 DataManager 与 EntityManager。 |
实际上,DataManager
只是委托给一个数据存储实现,并在需要时处理跨数据库引用。当使用标准 RdbmsStore
处理存储在关系型数据库中的实体时,下面描述的大多数实现细节都有效。对于另一种类型的数据存储,除接口方法名称之外的所有内容都可能不同。为简单起见,DataManager 在没有另外说明时指的是 基于 RdbmsStore 的 DataManager。
下面列出了 DataManager
的方法:
-
load(Class)
- 加载指定类的实体。此方法是流式 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)
- 通过查询纯数值加载键值对。此方法是流式 API 的入口点:List<KeyValueEntity> list = dataManager.loadValues( "select o.customer, sum(o.amount) from demo$Order o " + "where o.date >= :date group by o.customer") .store("legacy_db") (1) .properties("customer", "sum") (2) .parameter("date", orderDate) .list();
1 - 指定实体所在的数据存储。 如果实体位于主数据存储,那么可以忽略这个方法。 2 - 指定返回的 KeyValueEntity
实体中的属性名称。 属性的顺序必须与查询结果集的列对应。 -
loadValue(String query, Class valueType)
- 通过查询纯数值加载单个值。此方法是流式 API 的入口点:BigDecimal sum = dataManager.loadValue( "select sum(o.amount) from demo$Order o " + "where o.date >= :date group by o.customer", BigDecimal.class) .store("legacy_db") (1) .parameter("date", orderDate) .one();
1 - 指定实体所在的数据存储。 如果实体位于主数据存储,那么可以忽略这个方法。 -
load(LoadContext)
,loadList(LoadContext)
– 根据传递给它的LoadContext
对象的参数加载实体。LoadContext
必须包含 JPQL 查询语句或实体标识符。如果两者都定义的话,则使用查询语句而忽略实体标识符。例如:
@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)
- 加载键值对列表。该方法接受ValueLoadContext
,定义纯数值的查询语句和键值列表。返回包含KeyValueEntity
实例的列表。例如: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)
- 返回传递给方法的查询语句的记录数。可能的情况下,RdbmsStore
中的标准实现使用与原始查询相同的条件执行select count()
查询,以获得最佳性能。 -
commit(CommitContext)
– 将CommitContext
中传递的一组实体保存到数据库中。必须分别指定用于更新和删除的实体的集合。该方法返回 EntityManager.merge() 返回的实体实例集合,实际上这些就是刚刚在 DB 中更新的新实例。进一步的操作需要使用这些返回的实例,以防止数据丢失或造成乐观锁。通过使用
CommitContext.getViews()
获得的视图映射为每个保存的实例设置视图,这样可以确保返回实体中包含需要的属性。保存实体集合的示例:
@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)
- 使用视图从数据库重新加载指定实例的便捷方法。委托给load()
方法执行。 -
remove(Entity)
- 从数据库中删除指定的实例。委托给commit()
方法执行。 -
create(Class)
- 在内存中创建给定实体的实例。这是一个便捷的方法,委托给了Metadata.create()
。 -
getReference(Class, Object)
- 返回一个实体实例,该实例可以用作对数据库中存在的对象的引用。例如,如果要创建
User
,则必须设置用户所属的Group
。如果知道 group ID,可以从数据库加载然后设置给用户。此方法可以避免不必要的数据库多次访问:user.setGroup(dataManager.getReference(Group.class, groupId)); dataManager.commit(user);
引用也可用于通过 id 删除现有对象:
dataManager.remove(dataManager.getReference(Customer.class, customerId));
- 查询
-
JPQL 查询的规则与 执行 JPQL 查询 中描述的规则类似。不同之处在于,通过
DataManager
执行的查询只能使用命名参数,不支持位置参数。
- 事务
-
DataManager 总是启动一个新的事务并在操作完成时提交事务,从而返回游离状态的实体。在中间层,如果需要实现复杂的事务行为,可以使用 TransactionalDataManager。
- 部分实体
-
部分(Partial) 实体是一个实体实例,这个实例的属性可以是已加载的本地属性的一个子集。默认情况下,DataManager 根据视图加载部分实体(事实上,
RdbmsStore
只是将视图的 loadPartialEntities 属性设置为 true 并将其传递给 EntityManager )。在下面这些情况下,DataManager 会加载所有本地属性,视图仅用来获取引用:
3.2.6.2.1. DataManager 与 EntityManager
DataManager和EntityManager都可以用于实体的 CRUD 操作。这两个接口之间存在以下差异:
DataManager | EntityManager |
---|---|
DataManager 在中间层和客户端层都可用。 |
EntityManager 仅在中间层可用。 |
DataManager 是一个单例 bean。它可以通过 |
应该通过Persistence接口获取 EntityManager 的引用。 |
DataManager 定义了一些使用游离实体的高级方法: |
EntityManager 与标准的 |
实际上,DataManager 委托给DataStore实现,因此下面列出的 DataManager 功能仅适用于处理关系型数据库中的实体最常见的情况:
DataManager | EntityManager |
---|---|
DataManager 始终在内部启动新的事务。在中间层,如果需要实现复杂的事务行为,可以使用 TransactionalDataManager。 |
在使用 EntityManager 之前,必须先打开一个事务。 |
EntityManager 加载所有本地属性。如果指定了视图,则仅影响引用属性。详情请参阅这里。 |
|
DataManager 仅执行 JPQL 查询。此外,它有单独的加载实体的方法: |
EntityManager 可以运行任何 JPQL 或原生(SQL)查询。 |
在客户端层调用时,DataManager 会检查安全限制。 |
EntityManager 不会应用安全限制。 |
在客户端层处理数据时,只有一个选择 - DataManager
。在中间件层,当需要在事务内部实现某些原子逻辑或者 EntityManager
接口更适合该任务时,请使用 TransactionalDataManager
。通常来说,在中间件上两者都可以使用。
3.2.6.2.2. TransactionalDataManager
TransactionalDataManager
是一个中间层的 bean,跟 DataManager
接口类似,但是可以使用一个已经存在的事务。有如下功能:
-
如果有活跃的事务,则会使用它,否则跟
DataManager
一样会创建并提交一个新事务。 -
接收并返回游离态的实体。开发者需要使用合适的视图加载实体并使用
save()
方法显式的保存修改的实例至数据库。 -
使用行级安全机制,使用跟
DataManager
一样的方式处理动态属性和跨数据存储引用。
下面是在 service 的方法中使用 TransactionalDataManager
的简单示例:
@Inject
private TransactionalDataManager txDataManager;
@Transactional
public void transfer(Id<Account, UUID> acc1Id, Id<Account, UUID> acc2Id, Long amount) {
Account acc1 = txDataManager.load(acc1Id).one();
Account acc2 = txDataManager.load(acc2Id).one();
acc1.setBalance(acc1.getBalance() - amount);
acc2.setBalance(acc2.getBalance() + amount);
txDataManager.save(acc1);
txDataManager.save(acc2);
}
可以在框架测试代码中找到更复杂的例子: DataManagerTransactionalUsageTest.java
如果需要在当前事务中处理 EntityChangedEvent 事件的话, |
3.2.6.2.3. DataManager 安全机制
DataManager 的 load()
、loadList()
、loadValues()
和 getCount()
方法会检查用户是否对要加载的实体有 READ 权限。此外,从数据库加载实体也受访问组约束的限制。
commit()
方法检查新建实体需要的 CREATE 权限、更新实体需要的 UPDATE 权限和删除实体需要的 DELETE 权限。
默认情况下,在客户端调用时,DataManager
会检查实体操作(READ / CREATE / UPDATE / DELETE)的权限,但是在从中间件代码调用时忽略这些检查。默认情况不强制检查实体属性权限。
如果要在中间件代码中使用 DataManager
时检查实体操作权限,请通过 DataManager.secure()
方法获取包装类并调用其方法。或者,可以设置 cuba.dataManagerChecksSecurityOnMiddleware 应用程序属性以打开整个应用程序的安全检查。
只有在将 cuba.entityAttributePermissionChecking 应用程序属性设置为 true 时,才会在中间件上强制执行属性权限。如果中间件为理论上可以被攻击的远程客户端提供服务,比如桌面客户端(这是可能的)。在这种情况下,还要将 cuba.keyForSecurityTokenEncryption 应用程序属性设置为唯一值。如果应用程序仅使用 Web 或 Portal 客户端,则可以安全地保留这些属性的默认值。
请注意,无论上述条件如何始终都会实施访问组约束 (行级别安全性)。
3.2.6.2.4. 去重查询
如果一个界面包含带分页的表格,以及用于加载数据的 JPQL 查询,由于应用了通用过滤器或访问组约束,JPQL 查询在运行时会被修改。在 JPQL 查询中省略 distinct
运算符时会发生以下情况:
-
如果在数据库级别进行数据集合的组合,则加载的数据集将包含重复行。
-
在客户端级别,由于数据被添加到 map 中(
java.util.Map
),重复项在数据源中会消失。 -
对于分页表格,界面可能显示的行数少于请求的行数,而总行数超出请求的数量。
因此,我们建议在 JPQL 查询中包含 distinct
,以确保从数据库返回的数据集中不存在重复项。但是,如果返回的记录数很大(超过 10000),则在使用 distinct
执行 SQL 查询时,某些数据库服务器(特别是 PostgreSQL)会出现性能问题。
为了解决这个问题,平台包含一种在 SQL 级别上没有 distinct
的情况下正常运行的机制。此机制由 cuba.inMemoryDistinct 应用程序属性启用。启用后,会执行以下操作:
-
JPQL 查询仍然应包含
select distinct
。 -
在将数据发送到 ORM 之前,
DataManager
会从 JPQL 查询中删除distinct
。 -
数据页由
DataManager
加载后,会删除重复项在 DB 运行其它查询,用来检索返回给客户端需要的行数。
3.2.6.2.5. 级联查询(Sequential Queries)
DataManager
可以从之前的请求的结果中再次选择数据。此功能被通用过滤器用在连续使用过滤器的场景。
该机制的工作原理如下:
-
如果提供一个定义了
prevQueries
和queryKey
属性的LoadContext
,DataManager
将执行先前的查询并将检索到的实体的标识符保存在SYS_QUERY_RESULT
表中(对应于sys$QueryResult
实体),保存时会根据用户会话和查询会话的queryKey
分隔记录集。 -
修改当前查询以便与前一个查询的结果组合,这样能得到按照 AND 组合两个查询条件的结果。
-
之后重复该过程。在这种情况下,逐渐减少的之前的结果集会被从
SYS_QUERY_RESULT
表中删除并重填。
已经终止的用户会话在 SYS_QUERY_RESULT
表中留下的历史查询结果会被定期清理。这是由 QueryResultsManagerAPI
bean 的 deleteForInactiveSessions()
方法完成的,该方法由 cuba-spring.xml
中定义的 Spring 调度程序调用。 默认情况下,每 10 分钟执行一次,但可以使用 core 模块的 cuba.deleteOldQueryResultsInterval
应用程序属性按需要设置不同的时间间隔(以毫秒为单位)。
3.2.6.3. EntityStates
获取由 ORM 管理的持久化实体信息的接口。与 Persistence 和 PersistenceTools bean 不同,此接口可用于所有层。
EntityStates
接口具有以下方法:
-
isNew()
– 确定传递的实例是否是新创建的,即是否在 New 状态。如果此实例实际上处于 Managed 状态但在当前事务中刚被持久化,或者不是持久化实体,也会返回true
。 -
isManaged()
- 确定传递的实例是否被托管,比如是否添加到持久化上下文。 -
isDetached()
– 确定传递的实例是否处于游离状态。如果此实例不是持久化实体,也返回true
。 -
isLoaded()
- 确定是否从数据库加载了属性。属性已加载:如果属性包含在视图中,或者如果是本地属性并且未向加载机制(EntityManager 或 DataManager)提供视图。此方法只能检查实体的直接属性。 -
checkLoaded()
- 与isLoaded()
一样,但如果传递给方法的属性中至少有一个未加载,则抛出IllegalArgumentException
。 -
isLoadedWithView()
- 接收实例和视图作为参数,如果实际加载了视图所需的所有属性,则返回 true。 -
checkLoadedWithView()
- 与isLoadedWithView()
一样,只不过是抛出IllegalArgumentException
而不返回 false。 -
makeDetached()
- 接收新创建的实体实例并将其转换为游离状态。游离的对象可以传递给DataManager.commit()
或EntityManager.merge()
以将其状态保存在数据库中。请参阅 API 文档中的详细信息。 -
makePatch()
- 接受新创建的实体实例并使其成为 补丁对象(patch object)。该补丁对象可以传递给DataManager.commit()
或EntityManager.merge()
以将其状态保存在数据库中。与游离对象不同,补丁对象只保存非空属性。请参阅 API 文档中的详细信息。
3.2.6.4. Events
Events
bean 封装了应用程序范围内的事件发布功能。应用程序事件可用于在松耦合的组件之间交换信息。Events
bean 是 Spring Framework 的 ApplicationEventPublisher
的简单外观设计模式实现。
public interface Events {
String NAME = "cuba_Events";
void publish(ApplicationEvent event);
}
此接口只有接收一个事件对象作为参数的 publish()
方法。Events.publish()
会通知所有应用程序内注册的匹配的监听器。可以使用 PayloadApplicationEvent
将任何对象发布为事件。
另请参阅 Spring 框架入门
- bean 中的事件处理
-
首先,创建一个新的事件类,继承
ApplicationEvent
类。事件类可以包含任何其它数据。例如: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; } }
Bean 对象可以使用
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())); } }
默认情况下,所有事件都是同步处理的。
处理事件有两种方法:
-
实现
ApplicationListener
接口。 -
方法使用
@EventListener
注解。
在第一种情况下,必须创建一个实现
ApplicationListener
接口的 bean,以需要事件类型作为泛型参数:@Component public class DemoEventListener implements ApplicationListener<DemoEvent> { @Inject private Logger log; @Override public void onApplicationEvent(DemoEvent event) { log.debug("Demo event is published"); } }
第二种方法可用于隐藏实现接口的细节并在单个 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 } }
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.
如果确实需要在带有接口的 bean 中监听事件,可以通过实现
ApplicationListener
接口来代替。可以使用 Spring 框架的
Ordered
接口和@Order
注解来对事件处理方法排序。框架中所有 bean 和事件处理方法使用的是 100 到 1000 之间的order
值,因此可以在框架的 order 值之前或之后添加自定义的处理顺序。如果要在平台 bean 之前添加 bean 或事件处理程序,可使用小于 100 的值。另请参阅登录事件.
-
- UI 界面中的事件处理
-
通常,
Events
将事件的发布委托给ApplicationContext
执行。在 Web 层,可以为事件类使用特殊的接口 -UiEvent
。这是一个事件的标记接口,实现这个接口的事件被发送到当前 UI 实例中的 UI 界面(当前 Web 浏览器标签页)。请注意,UiEvent
实例不会发送到 Spring bean。示例事件类:
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; } }
可以在窗口的控制器中使用
Events
bean 触发事件,使用方式和在 bean 中一样:@Inject Events events; // ... UserRemovedEvent event = new UserRemovedEvent(this, removedUser); events.publish(event);
为了处理事件,必须在 UI 界面中使用注解
@EventListener
定义方法(不支持ApplicationListener
接口):@Order(15) @EventListener protected void onUserRemove(UserRemovedEvent event) { notifications.create() .withCaption("User is removed " + event.getUser()) .show(); }
可以使用
@Order
注解为事件监听器设定执行顺序。如果一个事件是
UiEvent
并使用来自 UI 线程的Events
bean 触发,那么带有@EventListener
方法的打开的窗口或 frame 将接收到该事件。事件处理是同步的。只有用户打开的当前 Web 浏览器标签页中 UI 界面才会收到该事件。
3.2.6.5. Messages
Messages
接口提供了获取本地消息字符串的方法。
主要方法如下:
-
getMessage()
– 根据消息键名、包名和需要的语言环境返回本地化的消息。该方法有几个重载,如果未在方法参数中指定语言环境,则使用当前用户语言设置。例如:
@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()
– 通过消息键名、包名和需要的语言环境检索本地化消息,然后使用获取到的消息格式化输入的参数。格式化消息根据String.format()
方法的规则定义。该方法有几个重载,如果未在方法参数中指定语言环境,则使用当前用户的语言设置。例如:
String formattedValue = messages.formatMessage(getClass(), "someFormat", someValue);
-
getMainMessage()
– 从应用程序 block 的主消息包返回本地化消息。例如:
protected Messages messages = AppBeans.get(Messages.class); ... messages.getMainMessage("actions.Ok");
-
getMainMessagePack()
– 返回应用程序 block 的主消息包的名称。例如:
String formattedValue = messages.formatMessage(messages.getMainMessagePack(), "someFormat", someValue);
-
getTools()
– 返回MessageTools
接口实例(见下文)。
3.2.6.5.1. MessageTools
MessageTools
接口是一个托管 Bean,包含用于处理本地化消息的其它方法。可以使用 Messages.getTools()
方法访问 MessageTools
接口,或像任何其它任何 bean 一样,通过注入的方式或 AppBeans
类来访问。
MessageTools
的方法:
-
loadString()
– 返回一个本地化的消息,由msg://{messagePack}/{key}
格式的消息引用指定消息引用字符串的结构:
-
msg://
– 必须的前缀。 -
{messagePack}
– 消息包的可选名称。如果未指定,则假定包名称作为单独的参数传递给loadString()
。 -
{key}
– 包中消息的键名。
消息引用字符串的示例:
msg://someMessage msg://com.abc.sales.web.customer/someMessage
-
-
getEntityCaption()
– 返回实体的本地化名称。 -
getPropertyCaption()
– 返回实体属性的本地化名称。 -
hasPropertyCaption()
– 检查实体属性是否被赋予了本地化名称。 -
getLocValue()
– 返回使用 @LocalizedValue 注解的实体属性的本地化值。 -
getDefaultLocale()
– 返回应用程序默认语言环境,这是 cuba.availableLocales 应用程序属性中列出的第一个语言环境。 -
useLocaleLanguageOnly()
– 如果应用程序支持的所有语言环境(在cuba.availableLocales
属性中定义)都只指定了语言参数,没有指定 country 和 variant,则返回true
。平台机制使用此方法,当语言环境信息是从外部源(例如操作系统或 HTTP 请求)接收到的,此机制需要确定一个最合适的支持语言。 -
trimLocale()
– 如果useLocaleLanguageOnly()
方法返回true
,则从传递的语言环境信息中清除除语言参数外的所有信息(country 和 variant)。
在应用程序中可以通过 覆盖 MessageTools
bean 来扩展它的方法集。下面是使用扩展接口的示例:
MyMessageTools tools = messages.getTools();
tools.foo();
((MyMessageTools) messages.getTools()).foo();
3.2.6.6. Metadata
Metadata
接口提供对元数据会话和视图仓库的访问功能。
接口方法:
-
getSession()
– 返回元数据会话实例。 -
getViewRepository()
– 返回视图仓库实例。 -
getExtendedEntities()
– 返回ExtendedEntities
类实例,用于处理扩展实体。在扩展实体章节查看更多信息。 -
create()
– 创建一个实体实例,此方法考虑了可能的实体扩展。对于持久化的
BaseLongIdEntity
和BaseIntegerIdEntity
子类,在创建后立即分配标识符。新标识符是通过自动创建的数据库序列获取的。默认情况下,序列在主数据存储中创建。但是,如果 cuba.useEntityDataStoreForIdSequence 应用程序属性设置为 true,则会在实体所属的数据存储中创建序列。 -
getTools()
– 返回MetadataTools
接口实例(见下文)。
3.2.6.6.1. MetadataTools
MetadataTools
是一个托管 Bean,包含了处理元数据的其它方法。可以通过使用 Metadata.getTools()
方法访问 MetadataTools
接口,也可以像其它 bean 一样,通过注入或 AppBeans
类来获取。
MetadataTools
接口的方法:
-
getAllPersistentMetaClasses()
– 返回持久化实体元类的集合。 -
getAllEmbeddableMetaClasses()
– 返回可嵌入实体元类的集合。 -
getAllEnums()
– 返回用作实体属性类型的枚举类的集合。 -
format()
– 根据给定元属性(meta-property) 的数据类型格式化传递的值。 -
isSystem()
– 检查元属性是否是系统级属性,即在基础实体接口中定义的。 -
isPersistent()
– 检查元属性是否是持久化属性,即属性有对应的数据库字段。 -
isTransient()
– 检查元属性或任意属性是否为非持久化的。 -
isEmbedded()
– 检查元属性是否为嵌入对象。 -
isAnnotationPresent()
– 检查在一个类或其父类中是否存在指定的注解。 -
getNamePatternProperties()
– 返回实例名称中包含的属性的元属性集合,实例名由Instance.getInstanceName()
方法返回。请参阅 @NamePattern。
可以通过在应用程序中 覆盖 MetadataTools
bean 来扩展其方法集。使用扩展接口的示例:
MyMetadataTools tools = metadata.getTools();
tools.foo();
((MyMetadataTools) metadata.getTools()).foo();
3.2.6.7. Resources
Resources
接口根据以下规则管理资源的加载:
-
如果提供的位置是 URL,则通过该 URL 下载资源;
-
如果提供的位置以
classpath:
前缀开头,则从类路径(classpath)加载资源; -
如果该位置不是 URL 并且它不以
classpath:
开头,那么:-
使用提供的位置作为相对路径,在应用程序的 配置文件目录 中搜索该文件。如果找到该文件,则从中下载资源;
-
如果在前面的步骤中找不到资源,则从类路径(classpath)下载。
-
实际上,很少使用显式的 URL 标识或 classpath:
前缀,因此通常可以通过配置文件目录或类路径下载资源。配置文件目录中的资源会覆盖类路径下相同名称的资源,即配置文件目录下的资源的优先级更高。
Resources
接口的方法:
-
getResourceAsStream()
– 返回指定资源的InputStream
,如果找不到资源则返回null
。应该在使用后关闭流,例如:@Inject protected Resources resources; ... InputStream stream = null; try { stream = resources.getResourceAsStream(resourceLocation); ... } finally { IOUtils.closeQuietly(stream); }
也可以使用 "try with resources" 的编码方式:
try (InputStream stream = resources.getResourceAsStream(resourceLocation)) { ... }
-
getResourceAsString()
– 则以字符串形式返回所指定的资源内容,如果没有找到资源,返回null
。
3.2.6.8. Scripting
Scripting
接口用于动态地编译和加载 Java 和 Groovy 类(在运行时),以及执行 Groovy 脚本和表达式。
Scripting
接口的方法:
-
evaluateGroovy()
– 执行 Groovy 表达式并返回执行结果。cuba.groovyEvaluatorImport 应用程序属性用于定义插入到每个被执行表达式中的导入类的公共集合。默认情况下,所有标准应用程序 block 都导入 PersistenceHelper 类。
已编译的表达式会被缓存,这大大加快了重复执行的速度。
例如:
@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()
– 执行 Groovy 脚本并返回执行结果。该脚本应位于应用程序 配置文件夹或类路径(classpath)中(当前的
Scripting
实现仅支持 JAR 文件中的类路径资源)。配置文件夹中的脚本将覆盖类路径中相同名称的脚本。脚本的路径是使用分隔符
/
构造的,路径的开头不需要分隔符。例如:
@Inject protected Scripting scripting; ... Binding binding = new Binding(); binding.setVariable("itemId", itemId); BigDecimal amount = scripting.runGroovyScript("com/abc/sales/CalculatePrice.groovy", binding);
-
loadClass()
– 使用以下步骤加载 Java 或 Groovy 类:-
如果类已加载,则返回该类。
-
在配置文件夹中搜索 Groovy 源代码(
*.groovy
文件)。如果找到 Groovy 文件,它将被编译并返回 class 文件。 -
在配置文件夹中搜索 Java 源代码(
*.java
文件)。如果找到它,它将被编译并返回 class 文件。 -
在类路径中搜索已编译的 class 文件。如果找到,它将被加载并返回。
-
如果找不到任何内容,将返回
null
。
可以在运行时修改配置文件夹中的包含 Java 和 Groovy 源代码的文件。在下一次调用
loadClass()
时,将重新编译相应的类,并返回新的类,这个机制有以下限制:-
源代码的类型不能从 Groovy 更改为 Java;
-
Groovy 源代码一旦编译,删除这个源代码文件不会导类重新加载,而是仍然返回之前编译好的类。
例如:
@Inject protected Scripting scripting; ... Class calculatorClass = scripting.loadClass("com.abc.sales.PriceCalculator");
-
-
getClassLoader()
– 返回一个类加载器(ClassLoader)
,它能够按照上述loadClass()
方法的规则加载类。
可以使用CachingFacadeMBean JMX bean 在运行时清理编译的类的缓存。
另请参阅 ScriptingManagerMBean。
3.2.6.9. Security
此接口用于检查用户对系统中不同对象的访问权限。大多数接口方法委托给当前 UserSession 对象的相应方法,但在此之前,会搜索实体的原始 meta-class,这对于具有扩展的项目很重要。 除了复制 UserSession
功能的方法之外,该接口还包含 isEntityAttrReadPermitted()
和 isEntityAttrUpdatePermitted()
方法,这两个方法用于检查指定属性路径的可用性,在检查的过程中会检查包含在这个路径中的所有实体和属性的可用性。
建议应用程序代码中使用 Security
接口,而不是直接调用 UserSession.isXYXPermitted()
方法。
在用户认证中查看更多信息。
3.2.6.10. TimeSource
TimeSource
接口用于获取当前时间。不建议在应用程序代码中使用 new Date()
或类似的方法来获取当时时间。
例如:
@Inject
protected TimeSource timeSource;
...
Date date = timeSource.currentTimestamp();
long startTime = AppBeans.get(TimeSource.class).currentTimeMillis();
3.2.7. 应用程序上下文(AppContext)
AppContext
是一个系统类,它在其静态字段中存储对每个应用程序block的某些公共组件的引用,包括:
-
Spring 框架的
ApplicationContext
。 -
从
app.properties
文件加载的应用程序属性集合。 -
ThreadLocal
变量,存储SecurityContext实例。 -
应用程序生命周期监听器的集合(
AppContext.Listener
)。
启动应用程序时,使用加载器类初始化 AppContext
,对于每个应用程序block:
-
中间件加载器 –
AppContextLoader
-
Web 客户端加载器 –
WebAppContextLoader
-
Web 门户加载器 –
PortalAppContextLoader
AppContext
可以在应用程序代码中用于以下任务:
-
将
SecurityContext
传递给新的执行线程,请参阅用户认证。 -
注册监听器,在完全初始化之后或应用程序终止之前触发,例如:
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"); } });
请注意,在应用程序启动和关闭时执行代码的推荐方法是使用应用程序生命周期事件。
3.2.8. 应用程序生命周期事件
CUBA 应用程序中有以下几种类型的生命周期事件:
- AppContextInitializedEvent
-
在AppContext初始化后立即触发。 此时:
-
所有bean都已完全初始化,并且执行了
@PostConstruct
方法。 -
可通过静态方法
AppBeans.get()
获取 bean。 -
AppContext.isStarted()
方法返回false
。 -
AppContext.isReady()
方法返回false
。
-
- AppContextStartedEvent
-
在
AppContextInitializedEvent
之后并且运行完所有AppContext.Listener.applicationStarted()
之后触发。此时:-
AppContext.isStarted()
方法返回true
。 -
AppContext.isReady()
方法返回false
。 -
在中间件上,如果启用了 cuba.automaticDatabaseUpdate应用程序属性,所有数据库更新脚本已成功执行。
-
- AppContextStoppedEvent
-
在应用程序关闭之前并且运行完所有
AppContext.Listener.applicationStopped()
之后触发。此时:-
所有 bean 都可以运行并且可以通过
AppBeans.get()
方法获得。 -
AppContext.isStarted()
方法返回false
。 -
AppContext.isReady()
方法返回false
。
-
通过指定 @Order
注解可以影响监听器调用的顺序。Events.HIGHEST_PLATFORM_PRECEDENCE
和 Events.LOWEST_PLATFORM_PRECEDENCE
常量指定平台中定义的监听器的使用范围。
例如:
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
-
在 servlet 和应用程序上下文初始化之后触发。此时:
-
可以通过静态方法
AppBeans.get()
获取 bean。 -
此事件包含应用程序和 servlet 上下文,因此可以注册自定义 servlet、过滤器和监听器,请参阅Servlet 和过滤器的注册。
-
- ServletContextDestroyedEvent
-
在 Servlet 和应用程序即将关闭时触发,在这个阶段可以手动释放资源。
例如:
@Component public class MyInitializerBean { @Inject private Logger log; @EventListener public void foo(ServletContextInitializedEvent e) { log.info("Application and servlet context is initialized"); } @EventListener public void bar(ServletContextDestroyedEvent e) { log.info("Application is about to shut down, all contexts are now destroyed"); } }
3.2.9. 应用程序属性
应用程序属性表示不同类型的命名值,它们决定着应用程序配置和功能的各个方面。平台广泛使用应用程序属性,还可以使用它们来配置应用程序的某些特性。
平台应用程序属性可按预期目的进行分类,如下所示:
-
配置参数 – 指定配置文件集合和一些用户界面参数,即确定应用程序功能。通常在开发时为应用程序项目定义配置参数的值。
-
部署参数 – 描述连接应用程序block的各种 URL、DBMS 类型、安全设置等。部署参数的值通常取决于安装应用程序实例的环境。
-
运行时参数 – 审计设置、电子邮件发送参数等。可以在应用程序运行时根据需要更改这些属性的值而不必重新启动应用程序。
- 设置应用程序属性
-
某些属性不支持设置在数据库中,原因是:在应用程序代码还无法访问数据库时就需要使用这些属性值。这些属性是上面提到的配置和部署参数。因此,只能在属性文件中或通过 Java 系统属性定义它们。运行时参数始终设置在数据库中(可能被文件或系统属性中的值覆盖)。
通常情况下,应用程序属性用于一个或多个应用程序block。例如cuba.persistenceConfig仅用于中间件,cuba.web.appWindowMode用于 Web 客户端,而cuba.springContextConfig用于所有的 block。这意味着如果你需要为一个属性设置值,就应该在所有使用这个属性的block中进行设置。存储在数据库中的属性可自动用于所有 block,因此可以仅在一个位置设置它们的值(在数据库表中),而不用关心哪个 block 将会使用它们。而且,还有一个标准 UI 界面来管理此类属性:请参考 Administration > Application Properties 。存储在文件中的属性应分别在各 block 的相应文件中设置。
当需要为平台属性设置值时,请在此文档中找到相关属性。如果文档声明该属性存储在数据库中,请使用 Administration > Application Properties 界面设置其值。否则,找出哪些 block 使用该属性并在这些 block 的
app.properties
文件中定义它。例如,如果文档声明该属性在所有 block 中使用,并且应用程序由中间件和 Web 客户端组成,则应在 core 模块的app.properties
文件和 web 模块的web-app.properties
文件中定义该属性。部署参数也可以在项目文件之外的配置目录中设置。有关详细信息,请参阅在文件中存储属性。
- 应用程序组件属性
-
应用程序组件可以通过在app-component.xml文件定义属性来对外暴露这些属性。 如果使用该组件的应用程序没有为这些属性指定值,则从该组件获取这些属性值。如果应用程序使用的多个组件定义了相同的属性,则应用程序中的实际值将从组件依赖关系层次结构从最近的组件中获得。如果多个组件位于依赖层次中的同一级,则该属性值会变得不确定。
- 累加 Properties
-
有时,需要从项目中使用的所有应用程序组件中获取属性值的组合。特别是应用程序的行为依赖于多个组件的同一属性的情况,平台需要读取多个组件的同一属性来配置应用程序。
通过在属性值的开头添加加号来使这些属性成为_累加_属性。此符号表示在运行时从应用程序组件组装属性值。例如,cuba.persistenceConfig是一个累加属性。在项目中,指定了一个定义项目数据模型的
persistence.xml
文件。但是由于实际的属性值还包括应用程序所依赖的应用程序组件的persistence.xml
文件,因此应用程序的完整数据模型还要包括组件中定义的实体。如果属性省略了
+
,则只能从当前项目获取其值。如果不想从组件继承某些配置(例如,在定义菜单结构时),它会很有用。在运行时获得的累加属性值是由 空格 连接的元素组成。
- 以编程的方式访问应用程序属性
-
可以使用以下机制访问代码中的应用程序属性:
-
Configuration 接口。如果将应用程序属性定义为配置接口的带注解方法,则应用程序代码将可对属性进行类型化访问。配置接口允许定义和访问所有存储类型的属性:数据库、文件和系统属性。
-
AppContext类的
getProperty()
方法。如果在文件或 Java 系统属性中设置属性,则可以使用此方法读属性值。该方法有以下弊端:-
不支持存储在数据库中的属性。
-
与调用接口方法不同,必须提供 String 类型的属性名称。
-
不能获取特定类型的属性值,只能获取 String 类型的属性值。
-
-
3.2.9.1. 在文件中存储属性
确定配置和部署参数的属性在特定的属性文件中指定,这些文件以 *app.properties
模式命名。每个应用程序block都包含一组此类文件,定义在web.xml的 appPropertiesConfig
参数中。
例如,中间件 block 的属性文件在 core 模块的 web/WEB-INF/web.xml
文件中指定,如下所示:
<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>
classpath:
前缀表示在 Java 类路径中查找相应的文件,而 file:
前缀表示它应该从文件系统加载。没有此前缀的路径表示相对于 Web 应用程序根目录的路径。可以使用 Java 系统属性:在此示例中,catalina.home
是 Tomcat 安装路径。
声明文件的顺序很重要,因为在每个后续文件中指定的值将覆盖前面文件中指定的相同名称的属性值。
上面集合中的最后一个文件是 local.app.properties
。它可用于在部署时覆盖应用程序属性。如果该文件不存在,则会忽略该它。可以在应用程序服务器上创建此文件,在其中定义特定于该环境的所有属性。这样,配置信息将与应用程序分离,能够做到更新应用程序而不必担心丢失特定于环境的配置信息。生产环境使用 Tomcat部分包含使用 local.app.properties
文件的示例。
创建
|
3.2.9.2. 在数据库中存储属性
表示运行时参数的应用程序属性存储在 SYS_CONFIG
表中。
这些属性具有以下特征:
-
由于属性值存储在数据库中,因此无论哪个应用程序 block 使用,都只要在一个地方定义。
-
可以通过以下方式在运行时更改和保存属性值:
-
使用 Administration > Application Properties 界面。
-
使用 ConfigStorageMBean JMX bean。
-
如果配置接口有 setter 方法,则可以在应用程序代码中设置属性值。
-
-
有两种方式重写属性值:在特定应用程序 block 的
*app.properties
文件中或 Java 系统属性中定义相同名称的属性
需要注意的是,客户端通过向中间件发送请求来访问存储在数据库中的属性,要比从本地 *app.properties
文件中检索属性效率低。为了减少请求数,客户端在配置接口实例生命周期内对属性进行了缓存。因此,如果需要从某个 UI 界面多次访问配置接口的属性,建议在界面初始化时获取配置接口的引用,并将其保存到界面控制器的字段中以便后续访问。
3.2.9.3. 配置接口
配置接口机制允许使用 Java 接口方法处理应用程序属性,从而带来以下好处:
-
类型化访问 - 在应用程序代码中可以使用实际数据类型(String 、 Boolean 、 Integer 等)。
-
应用程序代码使用接口方法而不是字符串类型的属性标识符,这些接口方法可由编译器检查,并且在集成开发环境中使用代码自动完成。
读取中间件 block 中的事务超时属性值的示例:
@Inject
private ServerConfig serverConfig;
public void doSomething() {
int timeout = serverConfig.getDefaultQueryTimeoutSec();
...
}
如果无法注入,可以通过Configuration基础接口获取配置接口引用:
int timeout = AppBeans.get(Configuration.class)
.getConfig(ServerConfig.class)
.getDefaultQueryTimeoutSec();
配置接口不是常规的 Spring 托管 bean。它们只能通过显式接口注入或通过 |
3.2.9.3.1. 使用配置接口
要在应用程序中创建配置接口,请执行以下操作:
-
创建一个继承自
com.haulmont.cuba.core.config.Config
的接口(不要与实体类com.haulmont.cuba.core.entity.Config
混淆)。 -
添加
@Source
注解以指定应属性值的存储位置:-
SourceType.SYSTEM
– 将使用System.getProperty()
方法从给定 JVM 的系统属性中获取值。 -
SourceType.APP
– 将从*app.properties
文件中获取值。 -
SourceType.DATABASE
– 将从数据库中获取值。
-
-
创建属性访问方法(getters / setters)。如果不打算通过代码更改属性值,那么就不要创建 setter 方法。getter 方法的返回类型即是属性的类型。可能的属性类型描述在这里。
-
添加
@Property
注解,定义 getter 方法对应的属性名称。 -
如果某个特定属性的来源与接口上定义的来源不同,可以为该属性单独设置
@Source
注解。 -
如果
@Source
值是SourceType.DATABASE
,则可以在平台提供的 Administration > Application Properties 界面上编辑该属性。可以使用@Secret
注解以掩码的方式在界面上显示属性值(将使用PasswordField而不是常规的文本字段)。
例如:
@Source(type = SourceType.DATABASE)
public interface SalesConfig extends Config {
@Property("sales.companyName")
String getCompanyName();
@Property("sales.ftpPassword")
@Secret
String getFtpPassword();
}
不要创建任何实现类,因为当注入配置接口或通过Configuration获取配置接口时,平台将自动创建所需的代理类。
3.2.9.3.2. 属性类型
平台支持以下开箱即用的属性类型:
-
String
, 原始类型及其封装类型(boolean
、Boolean
、int
、 `Integer`等) -
enum
,属性值作为枚举的值名称存储在文件或数据库中。如果枚举实现了
EnumClass
接口并且具有用于通过标识符获取值的静态方法fromId()
,则可以使用@EnumStore
注解指定存储枚举标识符而不是具体值。例如:@Property("myapp.defaultCustomerGrade") @DefaultInteger(10) @EnumStore(EnumStoreMode.ID) CustomerGrade getDefaultCustomerGrade(); @EnumStore(EnumStoreMode.ID) void setDefaultCustomerGrade(CustomerGrade grade);
-
持久化实体类。访问实体类型的属性时,将从数据库加载由属性值定义的实例。
要支持任意类型,请使用 TypeStringify
和 TypeFactory
类将值转换为字符串或从字符串转换值,并使用 @Stringify
和 @Factory
注解为属性指定这些类。
我们以 UUID
类型为例来了解这个过程。
-
创建类
com.haulmont.cuba.core.config.type.UuidTypeFactory
继承于com.haulmont.cuba.core.config.type.TypeFactory
类 并实现下面的方法:public Object build(String string) { if (string == null) { return null; } return UUID.fromString(string); }
-
在这种情况下没有必要创建
TypeStringify
,因为有toString()
方法。 -
在配置接口中注解属性:
@Factory(factory = UuidTypeFactory.class) UUID getUuidProp(); void setUuidProp(UUID value);
平台为以下类型提供了 TypeFactory
实现:
-
UUID
–UuidTypeFactory
, 如上所述。 -
java.util.Date
–DateFactory
。日期值必须以yyyy-MM-dd HH:mm:ss.SSS
格式指定,例如:cuba.test.dateProp = 2013-12-12 00:00:00.000
-
List<Integer>
(整数列表) –IntegerListTypeFactory
。必须以数字的形式指定属性值,用空格分隔,例如:cuba.test.integerListProp = 1 2 3
-
List<String>
(字符串列表) –StringListTypeFactory
。必须将属性值指定为由"|"分隔的字符串列表,例如:cuba.test.stringListProp = aaa|bbb|ccc
3.2.9.3.3. 默认值
可以为配置接口定义的属性指定默认值。如果未在存储位置(数据库或 *app.properties
文件)中设置该属性,则将返回这里定义的默认值而不是 null
。
可以使用 @Default
注解将默认值指定为字符串,或使用 com.haulmont.cuba.core.config.defaults
包中的其它注解将默认值指定为其它特定类型:
@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();
实体的默认值是 {entity_name}-{id}-{optional_view_name}
格式的字符串,例如:
@Default("sec$User-98e5e66c-3ac9-11e2-94c1-3860770d7eaf-browse")
User getAdminUser();
@Default("sec$Role-a294aef0-3ac9-11e2-9433-3860770d7eaf")
Role getAdminRole();
3.2.10. 消息本地化
基于 CUBA 框架的应用程序支持消息本地化,这意味着所有用户界面元素都可以以用户选择的语言显示。
语言选择选项由 cuba.localeSelectVisible 和 cuba.availableLocales 应用程序属性共同确定。
本节介绍本地化机制和本地化消息创建规则。有关获取消息的说明,请参阅 获取本地化消息。
3.2.10.1. 语言消息包
消息包是一组位于单个 Java 包中名称为 messages{_XX}.properties
格式的属性文件。XX
后缀表示在此文件中消息的语言,对应 Locale.getLanguage()
中的语言代码。也能够使用其它 Locale
属性,例如,country
。在这种情况下,消息包文件名看起来像 messages{_XX_YY}.properties
。如果包中的某个消息文件没有语言后缀 - 则是默认消息文件。消息的包名称对应于包含消息文件的 Java 包的名称。
我们看看以下示例:
/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
这个包由 4 个文件组成 - 一个用于俄语,一个用于法语,一个用于美式英语(带有美国国家代码),以及一个默认文件。包名是 com.abc.sales.gui.customer
。
消息文件包含键/值对,其中键是应用程序代码中引用消息的标识符,值是对应语言的消息本身。配对规则与 java.util.Properties
属性文件类似,同时具有以下特点:
-
文件编码 – 仅限
UTF-8
。 -
可以使用
@include
键引入其它消息包。可以使用以逗号分隔的列表包含多个包。在这种情况下,如果在当前包和引入包中都找到某个消息键,则使用来自当前包的消息。引入包的示例:@include=com.haulmont.cuba.web, com.abc.sales.web someMessage=Some Message ...
根据以下规则使用 Messages 接口方法来从包中检索消息:
-
首先在应用的配置目录中执行搜索。
-
在消息包名称指定的目录中搜索
messages_XX.properties
文件,其中XX
是所需语言的代码。 -
如果没有这样的文件,则在同一目录中搜索默认的
messages.properties
文件。 -
如果找到所需的语言文件或默认文件,则将其与所有
@include
文件一起加载,并在其中搜索消息键名。 -
如果没有找到文件或者文件中没有正确的消息键,则将目录更改为父目录并重复搜索过程。搜索将继续,直到到达配置目录的根目录。
-
-
如果在配置目录中没有找到该消息,则根据相同的算法在类路径中执行搜索。
-
如果找到该消息,则将其缓存并返回。如果没有,则消息不存在的事实也被缓存并返回搜索时传递的键。因此,复杂搜索过程仅执行一次,后续将从应用程序模块的本地高速缓存中加载结果。
建议按如下方式组织消息包:
|
3.2.10.2. 主语言消息包
每个标准的应用程序 block 都应该有它自己的 主(main) 消息包。对于客户端层的 block,主消息包包含主菜单条目和常用的 ui 元素名称(例如,ok 和 cancel 按钮的名称)。主程序包还决定所有应用程序模块(包括中间层)的数据类型转换格式。
cuba.mainMessagePack 应用程序属性用于指定主消息包。其属性值可以是单个包或由空格分隔的包名列表。例如:
cuba.mainMessagePack=com.haulmont.cuba.web com.abc.sales.web
在这种情况下,列表第二个包的消息将覆盖第一个包中的消息。因此,可以在应用程序项目覆盖应用程序组件中定义的消息。
通过在项目的主消息包中指定新消息也可以覆盖 CUBA 基础项目中已存在的消息。
com.haulmont.cuba.gui.backgroundwork/backgroundworkprogress.timeoutmessage = overridden error message
3.2.10.3. 实体和属性名称本地化
要在 UI 中显示实体和属性的本地化名称,请在包含实体的 Java 包中创建特殊的消息包。在消息文件中使用以下格式:
-
实体名称键 – 简单类名(不带包名)。
-
属性名称键 – 简单类名,后面跟上以句号分隔的属性名。
com.abc.sales.entity.Customer
实体的默认英文本地化示例 – /com/abc/sales/entity/messages.properties
文件:
Customer=Customer
Customer.name=Name
Customer.email=Email
Order=Order
Order.customer=Customer
Order.date=Date
Order.amount=Amount
此类消息包通常由框架隐式地使用,例如,被 Table 和 FieldGroup 可视化组件使用。除此之外,还可以使用以下方法来获取实体和属性的名称:
-
编程方式 – 通过 MessageTools
getEntityCaption()
、getPropertyCaption()
方法; -
在界面 XML 描述中 – 根据 MessageTools.loadString() 规则引用消息:
msg://{entity_package}/{key}
,例如:caption="msg://com.abc.sales.entity/Customer.name"
3.2.10.4. 枚举名称本地化
要本地化枚举的名称(names)和值(values),将具有以下键的消息添加到枚举类所在 Java 包的本地化消息包中:
-
枚举名称键 – 简单的类名(不带包名);
-
值健 – 简单类名,后面跟上以句号分隔的值名称。
例如,对于枚举
package com.abc.sales;
public enum CustomerGrade {
PREMIUM,
HIGH,
STANDARD
}
默认的英文本地化文件 /com/abc/sales/messages.properties
应包含以下行:
CustomerGrade=Customer Grade
CustomerGrade.PREMIUM=Premium
CustomerGrade.HIGH=High
CustomerGrade.STANDARD=Standard
本地化的枚举值可被不同的可视化组件自动利用,例如 LookupField。也可以通过编程方式获取本地化的枚举值:使用 Messages 接口的 getMessage()
方法并简单地将 enum
实例传递给它。
3.2.11. 用户认证
本节从开发人员的角度描述了一些访问控制方面的内容。有关配置用户数据访问限制的完整信息,请参阅安全子系统。
3.2.11.1. 用户会话
用户会话信息是 CUBA 应用程序访问控制机制的主要元素。它由 UserSession
对象表示,该对象与当前已通过身份验证的用户关联,并包含了用户的权限信息。在任何应用程序 block 中都可以使用 UserSessionSource 基础接口获取 UserSession
对象。
在使用用户名和密码对用户进行身份验证后,会执行 AuthenticationManager.login()
方法,此方法执行时会在中间件上创建 UserSession
对象。然后将对象缓存在中间件 block 中并返回到客户端层。在集群中运行时,会话对象将复制到所有集群成员。客户端层也会在接收会话对象之后将其存储下来,并以某种方式将其与活动用户相关联(例如,将其存储在 HTTP 会话中)。后续,此用户的所有中间件方法调用都会带上会话标识符( UUID
类型)。此过程不需要在应用程序代码中提供任何特殊处理,会话标识符会自动传递,与调用的方法的签名无关(即会话信息不通过方法参数传递)。中间件在处理客户端调用时,首先通过获得的标识符从缓存中检索会话,然后会话被与请求(http request)的执行线程关联。调用 AuthenticationManager.logout()
方法时或 cuba.userSessionExpirationTimeoutSec 应用程序属性定义的超时时间到期时,会从缓存中删除会话对象。
这样,在用户登录系统时创建的会话标识符用于在每次中间件调用期间进行用户验证。
UserSession
对象还包含当前用户_验证相关_的方法 - 验证对系统对象的访问权限: isScreenPermitted()
、isEntityOpPermitted()
、isEntityAttrPermitted()`和`isSpecificPermitted()
。但是,建议使用 Security 基础接口以编程的方式进行权限验证。
UserSession
对象可以包含任意可序列化类型的命名属性。属性由 setAttribute()
方法设置,并由 getAttribute()
方法获取。后者也能够像属性一样返回以下会话参数:
-
userId
– 当前注册的或代替的用户的 ID; -
userLogin
– 当前注册的或代替的用户登录名的小写形式。
会话属性与其它用户会话数据一样在中间件集群中被复制分发。
3.2.11.2. 登录
CUBA 框架提供内置的可扩展身份验证机制。这些机制包括不同的身份验证方案,例如登录/密码、记住账号、信任和匿名登录。
本节主要介绍中间层的身份验证机制。有关 Web 客户端身份验证机制的详细信息,请参阅 Web 登录。
平台在中间件包含以下身份验证机制:
-
由
AuthenticationManagerBean
实现的AuthenticationManager
。 -
AuthenticationProvider
实现。 -
由
AuthenticationServiceBean
实现的AuthenticationService
。 -
UserSessionLog
- 参阅用户会话日志。
此外,它还使用以下附加组件:
-
由
TrustedClientServiceBean
实现的TrustedClientService
- 为受信任客户端提供匿名会话或系统会话。 -
AnonymousSessionHolder
- 为受信任的客户端创建并保存匿名会话实例。 -
UserCredentialsChecker
- 检查用户凭据是否可以使用,比如可用于防止暴力破解。 -
UserAccessChecker
- 检查用户是否可以通过给定的上下文访问系统,例如,控制用户是否可以通过 REST 访问系统、控制指定的 IP 地址是否可以访问系统。
身份验证的主要接口是 AuthenticationManager
,它包含四个方法:
public interface AuthenticationManager {
AuthenticationDetails authenticate(Credentials credentials) throws LoginException;
AuthenticationDetails login(Credentials credentials) throws LoginException;
UserSession substituteUser(User substitutedUser);
void logout();
}
有两个方法具有相似的功能: authenticate()
和 login()
。两个方法都检查提供的凭据是否有效且对应于有效用户,然后返回 AuthenticationDetails
对象。它们之间的主要区别在于 login
方法还激活了用户会话,这样它随后就可以用于调用服务方法。
Credentials
表示用于身份验证子系统的一组凭据。平台有 AuthenticationManager
支持的以下几种类型的凭据:
适用于所有层:
-
LoginPasswordCredentials
-
RememberMeCredentials
-
TrustedClientCredentials
仅适用于中间层:
-
SystemUserCredentials
-
AnonymousUserCredentials
AuthenticationManager
的 login / authenticate 方法返回 AuthenticationDetails
实例,其中包含 UserSession 对象。此对象可用于检查其它权限、读取 User 属性和会话属性。平台只有一个内置的 AuthenticationDetails
接口实现 - SimpleAuthenticationDetails,它只存储用户会话对象,但是应用程序可以提供自己的带有附加信息的 AuthenticationDetails
实现。
AuthenticationManager 的 authenticate() 方法中执行以下三种操作之一:
-
如果可以验证输入的是一个有效用户,则返回
AuthenticationDetails
。 -
如果无法通过传递的凭据对象对用户进行身份验证,则抛出
LoginException
。 -
如果不支持传递的凭据对象,则抛出
UnsupportedCredentialsException
。
AuthenticationManager
的默认实现是 AuthenticationManagerBean
,它将身份验证委托给 AuthenticationProvider
实例链。 AuthenticationProvider
是一个可以处理特定 Credentials
实现的身份验证模块,它还有一个特殊的方法 supports()
,允许调用者查询它是否支持给定的 Credentials
类型。
标准的用户登录过程:
-
用户输入用户名和密码。
-
应用程序客户端使用用户名和密码作为参数调用
Connection.login()
方法。 -
Connection
创建Credentials
对象并调用AuthenticationService
的login()
方法。 -
AuthenticationService
将验证操作委托给AuthenticationManager
bean ,AuthenticationManager
bean 使用了AuthenticationProvider
对象链 。LoginPasswordAuthenticationProvider
可以使用LoginPasswordCredentials
对象。它通过输入的登录名加载User
对象,使用用户标识符作为盐值再次散列获得的密码哈希值,并将获得的哈希值与存储在 DB 中的密码哈希值进行比较。如果不匹配,则抛出LoginException
。 -
如果身份验证成功,则将用户的所有访问参数(角色列表、权限、约束和会话属性)加载到创建的 UserSession 实例中。
-
如果启用了用户会话日志,则会把包含用户会话信息的记录保存到数据库中。
另外请参阅 Web 登录过程。
密码散列算法由 EncryptionModule
类型 bean 实现,并在 cuba.passwordEncryptionModule 应用程序属性中指定。默认情况下使用 BCrypt。
- 内置验证提供程序
-
平台包含以下
AuthenticationProvider
接口的实现:-
LoginPasswordAuthenticationProvider
-
RememberMeAuthenticationProvider
-
TrustedClientAuthenticationProvider
-
SystemAuthenticationProvider
-
AnonymousAuthenticationProvider
所有实现都从数据库加载用户,使用
UserSessionManager
验证传递的凭据对象并创建非活动的用户会话。随后调用AuthenticationManager.login()
后,该会话实例将变为活动状态。LoginPasswordAuthenticationProvider
、RememberMeAuthenticationProvider
和TrustedClientAuthenticationProvider
使用额外检查插件:实现了UserAccessChecker
接口的 bean。如果有一个UserAccessChecker
实例抛出LoginException
,则认为验证失败并抛出LoginException
。此外,
LoginPasswordAuthenticationProvider
和RememberMeAuthenticationProvider
使用 UserCredentialsChecker beans 检查凭据实例。UserCredentialsChecker 接口只有一个内置实现 - BruteForceUserCredentialsChecker,用于检查用户是否使用暴力破解攻击来找出有效凭据。 -
- 异常类型
-
AuthenticationManager
和AuthenticationProvider
的authenticate()
方法和login()
方法会抛出 LoginException 或其子类异常。需要确认 此外,如果传递的凭据对象没有可用的AuthenticationProvider
bean,则抛出 UnsupportedCredentialsException。请参阅以下异常类:
-
UnsupportedCredentialsException
-
LoginException
-
AccountLockedException
-
UserIpRestrictedException
-
RestApiAccessDeniedException
-
- 事件
-
AuthenticationManager
的标准实现 -AuthenticationManagerBean
在登录或验证过程中触发以下应用程序 事件 :-
BeforeAuthenticationEvent
/AfterAuthenticationEvent
-
BeforeLoginEvent
/AfterLoginEvent
-
AuthenticationSuccessEvent
/AuthenticationFailureEvent
-
UserLoggedInEvent
/UserLoggedOutEvent
-
UserSubstitutedEvent
中间层的 Spring bean 可以使用 Spring
@EventListener
注解来处理这些事件:@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()); } }
上面提到的所有事件的事件处理器(不包括
AfterLoginEvent
、UserSubstitutedEvent
和UserLoggedInEvent
)都可以抛出LoginException
来中断身份验证/登录过程。例如,可以为应用程序实现一个维护模式开关,如果维护模式处于激活状态,它将阻止登录。
@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"); } } }
-
- 扩展点
-
可以使用以下类型的扩展点来扩展身份验证机制:
-
AuthenticationService
- 替换现有的AuthenticationServiceBean
。 -
AuthenticationManager
- 替换现有的AuthenticationManagerBean
。 -
AuthenticationProvider
实现类 - 实现额外的或替换现有的AuthenticationProvider
。 -
Events - 实现事件处理.
可以使用 Spring Framework 机制替换现有 bean,例如通过在 core 模块的 Spring XML 配置中注册新 bean。
<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); } }
事件处理器可以使用
@Order
注解来排序。所有平台 bean 和事件处理器都使用 100 到 1000 之间的order
值,因此可以在平台代码之前或之后添加自定义处理。如果要在平台 bean 之前添加 bean 或事件处理器 - 请使用小于 100 的值。事件处理器排序:
@Component public class DemoEventListener { @Inject private Logger log; @Order(10) @EventListener protected void onUserLoggedIn(UserLoggedInEvent event) { log.info("Demo"); } }
AuthenticationProvider 可以使用 Ordered 接口并实现
getOrder()
方法。@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; } }
-
- 额外功能
-
-
平台具有防止暴力破解密码的机制。通过中间件上的 cuba.bruteForceProtection.enabled 应用程序属性启用保护。如果启用了保护,则在多次登录尝试失败的情况下,用户登录名和 IP 地址的组合将被限制一段时间。 用户登录名和 IP 地址组合的最大登录尝试次数由 cuba.bruteForceProtection.maxLoginAttemptsNumber 应用程序属性定义(默认值为 5)。锁定间隔时间以秒为单位由 cuba.bruteForceProtection.blockIntervalSec 应用程序属性定义(默认值为 60)。
-
用户密码(实际上是密码哈希值)可能不存储在数据库中,而是通过外部手段验证,例如,通过 LDAP 集成的方式。在这种情况下,身份验证实际上是由客户端 block 执行的,而中间件通过使用带有
TrustedClientCredentials
的AuthenticationService.login()
方法创建基于用户登录名而没有密码的会话来“信任”客户端。此方法需要满足以下条件:-
客户端 block 必须传递所谓的受信任密码,该密码在中间件和客户端 block 的 cuba.trustedClientPassword 应用程序属性中指定。
-
客户端 block 的 IP 地址必须位于 cuba.trustedClientPermittedIpList 应用程序属性中指定的列表中。
-
-
对于计划的自动处理程序和使用 JMX 接口连接中间件 bean 也需要登录系统。事实上,这些操作被视为管理操作,只要数据库中没有更改实体,就不需要身份验证。当实体持久化到数据库时,该过程需要正在执行更改的用户登录,以保证登录的用户对存储的更改负责。
要求自动处理程序或 JMX 调用登录系统的另一个好处是,如果给执行线程设置了用户会话,服务器日志输出就可以显示出日志对应的用户信息。这对日志分析很有帮助,可以很方便地搜索特定处理程序产生的日志。
中间件中处理程序对系统的访问是使用
AuthenticationManager.login()
和SystemUserCredentials
完成的,SystemUserCredentials
包含将要执行处理程序的用户登录信息(无密码)。最终,会在相应的中间件 block 中创建并缓存 UserSession 对象,但这个对象不会在集群中分发。
可在 系统身份验证 中查看有关中间件处理身份验证的更多信息。
-
- Obsolete/Deprecated
-
以下组件已被弃用:
-
LoginService
将登录方法的执行委托给AuthenticationService
-
LoginWorker
将登录方法的执行委托给AuthenticationManager
不要在代码中使用这些组件。它们将在平台的下一个版本中删除。
-
3.2.11.3. 安全上下文
SecurityContext
类实例存储关于当前执行线程的用户会话信息。它在以下情况下被创建并传递给 AppContext.setSecurityContext()
方法:
-
对于 Web 客户端 block 和 Web 门户 block - 在开始处理用户浏览器的每个 HTTP 请求时。
-
对于中间件 block - 在开始处理来自客户端层和 CUBA 计划任务 的每个请求时。
在前两种情况下,当请求执行完成时,将从执行线程中删除 SecurityContext
。
如果通过应用程序代码创建一个新的执行线程,需要将当前的 SecurityContext
实例传递给它,示例如下:
final SecurityContext securityContext = AppContext.getSecurityContext();
executor.submit(new Runnable() {
public void run() {
AppContext.setSecurityContext(securityContext);
// business logic here
}
});
使用 SecurityContextAwareRunnable
或 SecurityContextAwareCallable
封装类可以完成同样的操作,例如:
executor.submit(new SecurityContextAwareRunnable<>(() -> {
// business logic here
}));
Future<String> future = executor.submit(new SecurityContextAwareCallable<>(() -> {
// business logic here
return some_string;
}));
3.2.12. 异常处理
本节介绍在 CUBA 应用程序中处理异常的各方面知识。
3.2.12.1. 异常类
创建自己的异常类时应该遵循以下规则:
平台包含一个特殊的非受检异常类 SilentException
。它可用于中断执行而不向用户显示任何消息或将其写入到日志。SilentException
声明在全局模块中,因此在中间层和客户端模块都能够访问到。
3.2.12.2. 传递中间件异常
如果从中间件抛出的异常是由于处理客户端请求引发的,则中止执行并且将异常对象返回给客户端。该对象通常包括底层异常链。此链可能包含客户端层不能够访问的类(例如,JDBC 驱动异常)。因此,平台不是发送此异常链到客户端,而是将其描述存储到专门创建的 RemoteException
对象中,将 RemoteException
对象发送到客户端。
导致异常的信息存储为 RemoteException.Cause
对象列表。每个 Cause
对象都包含一个异常类名及其消息。此外,如果异常类是 "客户端支持" 的,则 Cause
也会存储异常对象。这样可以在 Exception 对象的字段中传递信息给客户端。
如果异常类的对象应该作为 Java 对象传递给客户端层,则异常类应使用 @SupportedByClient
注解。例如:
@SupportedByClient
public class WorkflowException extends RuntimeException {
...
这样,当在中间件上抛出没有使用 @SupportedByClient
注解的异常时,客户端调用代码将接收到包含字符串形式的原始异常信息的 RemoteException
。 如果源异常带有 @SupportedByClient
注解,则调用者能直接接收到异常实例。这使得在客户端能够以传统方式(使用 try/catch 块)处理应用程序代码中间层服务声明的异常。
请注意,如果需要将客户端支持的异常作为对象传递给客户端。则它的 getCause()
链上不应该包含任何不支持的异常。因此,如果在中间层创建异常实例并且希望将其传递给客户端, 请确保 cause 参数包含的异常类型能被客户端识别。
ServiceInterceptor
类是一个服务拦截器,可以在异常对象传递给客户层之前将其打包。此外,它还执行异常日志记录。 默认情况下会记录有关异常的所有信息,包含完整的 stacktrace 日志。如果不需要,可添加 @Logging
注解到异常类并指定日志级别:
-
FULL
– 完整信息,包括 stacktrace(默认)。 -
BRIEF
– 仅包含异常类名称和消息。 -
NONE
– 没有输出。
例如:
@SupportedByClient
@Logging(Logging.Type.BRIEF)
public class FinancialTransactionException extends Exception {
...
3.2.12.3. 处理客户端层的异常
在客户端层抛出或从中间层传递的未处理的异常,将被传递给 Web 客户端 block 的特殊处理机制。
一个异常处理类是实现了 UiExceptionHandler
接口的托管 bean,在其 handle()
方法中执行处理异常的逻辑并返回 true
,或者如果这个处理器无法处理传递的异常,则立即返回 false
。这个行为能够为处理器创建一个 "责任链"。
建议这个处理器继承 AbstractUiExceptionHandler
基类,该基类能够解析异常链(包括打包在 RemoteException
中的异常链)并且可以处理特定的异常类型。此处理器支持的异常类型是通过将字符串数组从处理器的构造器传递给基类构造器来定义的。数组中的每个字符串应包含一个要处理的异常的完整类名。
假设有如下异常类:
package com.company.demo.web;
public class ZeroBalanceException extends RuntimeException {
public ZeroBalanceException() {
super("Insufficient funds in your account");
}
}
那么处理此异常的处理器必须有如下构造器:
@Component("demo_ZeroBalanceExceptionHandler")
public class ZeroBalanceExceptionHandler extends AbstractUiExceptionHandler {
public ZeroBalanceExceptionHandler() {
super(ZeroBalanceException.class.getName());
}
...
如果在客户端无法访问异常类,请使用字符串指定其名称:
@Component("sample_ForeignKeyViolationExceptionHandler")
public class ForeignKeyViolationExceptionHandler extends AbstractUiExceptionHandler {
public ForeignKeyViolationExceptionHandler() {
super("java.sql.SQLIntegrityConstraintViolationException");
}
...
在使用 AbstractUiExceptionHandler
作为基类的情况下,业务逻辑在 doHandle()
方法中,如下所示:
package com.company.demo.web;
import com.haulmont.cuba.gui.Notifications;
import com.haulmont.cuba.gui.exception.AbstractUiExceptionHandler;
import org.springframework.stereotype.Component;
import javax.annotation.Nullable;
@Component("demo_ZeroBalanceExceptionHandler")
public class ZeroBalanceExceptionHandler extends AbstractUiExceptionHandler {
public ZeroBalanceExceptionHandler() {
super(ZeroBalanceException.class.getName());
}
@Override
protected void doHandle(String className, String message, @Nullable Throwable throwable, UiContext context) {
context.getNotifications().create(Notifications.NotificationType.ERROR)
.withCaption("Error")
.withDescription(message)
.show();
}
}
如果异常类的名称不足以确定是否可以将此处理器应用于此异常,则应该定义 canHandle()
方法。此方法也接收异常的文本信息。如果处理器能够应用于此异常,则方法必须返回 true
。例如:
package com.company.demo.web.exceptions;
import com.haulmont.cuba.gui.Notifications;
import com.haulmont.cuba.gui.exception.AbstractUiExceptionHandler;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import javax.annotation.Nullable;
@Component("demo_ZeroBalanceExceptionHandler")
public class ZeroBalanceExceptionHandler extends AbstractUiExceptionHandler {
public ZeroBalanceExceptionHandler() {
super(ZeroBalanceException.class.getName());
}
@Override
protected void doHandle(String className, String message, @Nullable Throwable throwable, UiContext context) {
context.getNotifications().create(Notifications.NotificationType.ERROR)
.withCaption("Error")
.withDescription(message)
.show();
}
@Override
protected boolean canHandle(String className, String message, @Nullable Throwable throwable) {
return StringUtils.containsIgnoreCase(message, "Insufficient funds in your account");
}
}
通过 doHandle()
方法的 UiContext
参数可以获取到 Dialogs
接口,此接口提供了一个用来展示异常的特殊对话框,对话框中包含了一个可折叠的区域,这里能展示异常所有堆栈信息。这个对话框用在默认的异常处理器中,但是也可以为自己定义的异常使用,示例:
@Override
protected void doHandle(String className, String message, @Nullable Throwable throwable, UiContext context) {
if (throwable != null) {
context.getDialogs().createExceptionDialog()
.withThrowable(throwable)
.withCaption("Error")
.withMessage(message)
.show();
} else {
context.getNotifications().create(Notifications.NotificationType.ERROR)
.withCaption("Error")
.withDescription(message)
.show();
}
}
3.2.13. Bean 验证
Bean 验证是一种可选机制,可在通用 UI 和 REST API 中提供中间件上数据的统一验证。它基于 JSR 349 - Bean Validation 1.1 及其参考实现: Hibernate Validator。
3.2.13.1. 定义约束
可以使用 javax.validation.constraints
包中的注解或者自定义注解来定义约束。可以在一个实体或 POJO 类声明、字段或 getter 方法以及中间件服务方法上设置注解。
在实体字段上使用标准验证注解的示例:
@Table(name = "DEMO_CUSTOMER")
@Entity(name = "demo$Customer")
public class Customer extends StandardEntity {
@Size(min = 3) // length of value must be longer then 3 characters
@Column(name = "NAME", nullable = false)
protected String name;
@Min(1) // minimum value
@Max(5) // maximum value
@Column(name = "GRADE", nullable = false)
protected Integer grade;
@Pattern(regexp = "\\S+@\\S+") // value must conform to the pattern
@Column(name = "EMAIL")
protected String email;
//...
}
使用自定义类级别注解的示例(见下文):
@CheckTaskFeasibility(groups = {Default.class, UiCrossFieldChecks.class}) // custom validation annotation
@Table(name = "DEMO_TASK")
@Entity(name = "demo$Task")
public class Task extends StandardEntity {
//...
}
验证服务方法的参数和返回值的示例
public interface TaskService {
String NAME = "demo_TaskService";
@Validated // indicates that the method should be validated
@NotNull
String completeTask(@Size(min = 5) String comment, @Valid @NotNull Task task);
}
如果需要方法参数的级联验证,可以使用 @Valid
注解,在上面的例子中,还将验证声明在 Task
对象上的约束。
- 约束组
-
约束组允许根据应用程序逻辑仅应用所有已定义约束的子集。例如,可能想强制用户输入实体属性的值,但是同时又能够通过某种内部机制设置此属性为空,为此,应该在约束注解上指定
groups
属性。然后,只有将相同的组传递给验证机制时,约束才会生效。平台将以下约束组传递给验证机制:
-
RestApiChecks
- 在 REST API 中验证时。 -
ServiceParametersChecks
- 验证服务参数时。 -
ServiceResultChecks
- 验证服务返回值时。 -
UiComponentChecks
- 验证单个 UI 字段时。 -
UiCrossFieldChecks
- 在实体编辑器提交时进行类级别约束验证时。 -
javax.validation.groups.Default
- 除了 UI 编辑器上的提交操作之外,都会传递这个组。
-
- 验证消息
-
约束可包含要显示给用户的消息。
消息可以直接在验证注解上设置,例如:
@Pattern(regexp = "\\S+@\\S+", message = "Invalid format") @Column(name = "EMAIL") protected String email;
也可以将消息放在本地化消息包中并且使用以下格式在注解中指定消息:
{msg://message_pack/message_key}
或简单的{msg://message_key}
(仅用于实体中)。例如:@Pattern(regexp = "\\S+@\\S+", message = "{msg://com.company.demo.entity/Customer.email.validationMsg}") @Column(name = "EMAIL") protected String email;
或者,如果为实体定义约束并且消息在实体消息包中:
@Pattern(regexp = "\\S+@\\S+", message = "{msg://Customer.email.validationMsg}") @Column(name = "EMAIL") protected String email;
消息可以包含参数和表达式。参数包含在
{}
中,可使用的参数包括本地化消息或注解参数,例如{min}
、{max}
、{value}
。表达式包含在${}
中并且可以包含验证值变量(validatedValue
) 、注解参数(如value
或min
) 和 JSR-341 (EL 3.0)表达式。例如:@Pattern(regexp = "\\S+@\\S+", message = "Invalid email: ${validatedValue}, pattern: {regexp}") @Column(name = "EMAIL") protected String email;
本地化消息值也可以包含参数和表达式。
- 自定义约束
-
可以使用编程或声明式验证来创建自己的特定领域约束。
要以编程方式的验证器创建约束,请执行以下操作:
-
在项目的 global 模块中创建注解。使用
@Constraint
进行标注。这个注解必须包含message
、groups
和payload
属性:@Target({ ElementType.TYPE }) @Retention(RUNTIME) @Constraint(validatedBy = TaskFeasibilityValidator.class) public @interface CheckTaskFeasibility { String message() default "{msg://com.company.demo.entity/CheckTaskFeasibility.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
-
在项目的 global 模块中创建验证器类:
public class TaskFeasibilityValidator implements ConstraintValidator<CheckTaskFeasibility, Task> { @Override public void initialize(CheckTaskFeasibility constraintAnnotation) { } @Override public boolean isValid(Task value, ConstraintValidatorContext context) { Date now = AppBeans.get(TimeSource.class).currentTimestamp(); return !(value.getDueDate().before(DateUtils.addDays(now, 3)) && value.getProgress() < 90); } }
-
使用注解:
@CheckTaskFeasibility(groups = UiCrossFieldChecks.class) @Table(name = "DEMO_TASK") @Entity(name = "demo$Task") public class Task extends StandardEntity { @Future @Temporal(TemporalType.DATE) @Column(name = "DUE_DATE") protected Date dueDate; @Min(0) @Max(100) @Column(name = "PROGRESS", nullable = false) protected Integer progress; //... }
还可以使用现有的约束的组合来创建自定义约束,例如:
@NotNull @Size(min = 2, max = 14) @Pattern(regexp = "\\d+") @Target({METHOD, FIELD, ANNOTATION_TYPE}) @Retention(RUNTIME) @Constraint(validatedBy = {}) public @interface ValidProductCode { String message() default "{msg://om.company.demo.entity/ValidProductCode.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
当使用复合约束时,生成的“约束违反”集合将包含每个约束的“约束违反”。如果想返回单个“约束违反”,请使用
@ReportAsSingleViolation
注解这个复合注解类。 -
- CUBA 定义的验证注解
-
除了使用
javax.validation.constraints
包中的标准注解之外,可以使用在 CUBA 框架中定义的以下注解:-
@RequiredView
- 可以添加到服务方法定义中,以确保实体实例加载了视图中指定的所有属性。如果注解标记到方法上,则检查返回值。如果注解标记到参数上,则检查参数。如果返回值或者参数是集合,则检查集合中的所有元素。例如:
public interface MyService { String NAME = "sample_MyService"; @Validated void processFoo(@RequiredView("foo-view") Foo foo); @Validated void processFooList(@RequiredView("foo-view") List<Foo> fooList); @Validated @RequiredView("bar-view") Bar loadBar(@RequiredView("foo-view") Foo foo); }
-
3.2.13.2. 运行时验证
- 在 UI 中验证
-
连接到数据源的通用 UI 组件获取
BeanValidator
实例来检查字段的值。验证器是从可视化组件实现的Component.Validatable.validate()
方法调用的。如果验证不通过,会抛出CompositeValidationException
异常,这个异常实例中包含了一组违规信息的集合。可以移除标准的验证器,也可以使用不同的约束组初始化标准验证器:
@UiController("sample_NewScreen") @UiDescriptor("new-screen.xml") public class NewScreen extends Screen { @Inject private TextField<String> field1; @Inject private TextField<String> field2; @Subscribe protected void onInit(InitEvent event) { field1.getValidators().stream() .filter(BeanPropertyValidator.class::isInstance) .forEach(field1::removeValidator); (1) field2.getValidators().stream() .filter(BeanPropertyValidator.class::isInstance) .forEach(validator -> { ((BeanPropertyValidator) validator).setValidationGroups(new Class[] {UiComponentChecks.class}); (2) }); } }
1 从 UI 组件中完全删除 bean 验证。 2 这里,验证器将仅检查显式设置了 UiComponentChecks 组的约束,因为没有传递默认组。 默认情况下,
BeanValidator
具有Default
和UiComponentChecks
分组。如果实体属性带有
@NotNull
注解且没有定义约束组,则在元数据中这个属性会被标记为强制的(mandatory),并且通过数据源使用此属性的 UI 组件将具有required = true
属性。DateField和DatePicker组件使用
@Past
和@Future
注解自动设置其rangeStart
和rangeEnd
属性,不过这里忽略了时间部分。如果约束包含
UiCrossFieldChecks
组并且所有属性级别的检查都通过了,编辑界面将在提交时做类级别约束的验证。可以在 XML 描述或界面控制器使用crossFieldValidate
属性关闭此验证:<window xmlns="http://schemas.haulmont.com/cuba/window.xsd" caption="msg://editorCaption" class="com.company.demo.web.task.TaskEdit" datasource="taskDs" crossFieldValidate="false"> <!-- ... --> </window>
public class TaskEdit extends StandardEditor<Task> { @Subscribe protected void onInit(InitEvent event) { setCrossFieldValidate(false); } }
- 中间件服务验证
-
如果服务接口中的方法带有
@Validated
注解,则中间件服务会对方法的参数和返回结果执行验证。例如:public interface TaskService { String NAME = "demo_TaskService"; @Validated @NotNull String completeTask(@Size(min = 5) String comment, @NotNull Task task); }
@Validated
注解可以指定约束组以使验证应用到某组约束上,如果没有指定任何组,默认使用以下约束组:-
Default
和ServiceParametersChecks
- 进行方法参数验证时 -
Default
和ServiceResultChecks
- 进行方法返回值验证时
在验证错误时会抛出
MethodParametersValidationException
和MethodResultValidationException
异常。如果要在服务中以编程的方式执行某些自定义验证,请使用
CustomValidationException
来通知客户端有关验证的错误信息,这样可以与标准 bean 验证错误信息保持相同的格式。此异常也可以跟 REST API 客户端有特定的关联。 -
- 在 REST API 中验证
-
对于创建和更新操作, 通常 REST API 会自动执行 bean 验证。验证错误会以如下方式返回给客户端:
-
MethodResultValidationException
和ValidationException
导致500 Server error
HTTP 状态 -
MethodParametersValidationException
、ConstraintViolationException
和CustomValidationException
导致400 Bad request
HTTP 状态 -
格式为
Content-Type: application/json
的响应体将包含一个对象列表,每个对象都包含属性message
、messageTemplate
、path
和invalidValue
属,例如:[ { "message": "Invalid email: aaa", "messageTemplate": "{msg://com.company.demo.entity/Customer.email.validationMsg}", "path": "email", "invalidValue": "aaa" } ]
-
path
- 表示被验证对象的无效属性在对象关系图中的路径。 -
messageTemplate
- 消息模板字符串,这个模板字符串是在message
注解属性中定义。 -
message
- 包含验证消息的实际值 。 -
invalidValue
- 属性值类型是String
、Date
、Number
、Enum
、UUID
中的其中之一时才返回。
-
-
- 以编程的方式进行验证
-
可以使用
BeanValidation
基础设施接口以编程的方式执行验证。该接口可在中间件和客户端层使用。它用于获取执行验证的javax.validation.Validator
实现。验证的结果是一组ConstraintViolation
对象。例如:@Inject private BeanValidation beanValidation; public void save(Foo foo) { Validator validator = beanValidation.getValidator(); Set<ConstraintViolation<Foo>> violations = validator.validate(foo); // ... }
3.2.14. 实体属性访问控制
安全子系统 允许根据用户权限设置对实体属性的访问。也就是说,框架可以根据分配给当前用户的角色自动将属性设置为只读或隐藏。但有时可能还想根据实体或其关联实体的当前状态动态更改对属性的访问。
该机制的工作原理如下:
-
当 DataManager 加载一个实体时,它会找到实现
SetupAttributeAccessHandler
接口的所有托管 bean,并传递SetupAttributeAccessEvent
对象作为参数调用它们的setupAccess()
方法。此对象包含处于托管状态的已加载实例,以及三个用于存储属性名称的集合:只读属性集合、隐藏属性集合和必须属性集合(这些集合最初为空)。 -
SetupAttributeAccessHandler
接口的实现类分析实体的状态并适当地填充事件对象中的属性名称集合。这些类实际上是用于定义给定实例的属性访问规则的容器。 -
该机制将由规则定义的属性名称保存在实体实例本身(在关联的
SecurityState
对象中)。 -
在客户端层,通用 UI 和 REST API 使用
SecurityState
对象来控制对实体属性的访问。
要为特定实体类型创建规则,请执行以下操作:
-
在项目的 core 模块中创建托管 Bean 并实现
SetupAttributeAccessHandler
接口。使用被处理实体的类型对接口进行参数化。bean 范围(scope)必须是默认的单例(singleton)。必须实现接口方法:-
supports(Class)
如果处理器设计为处理给定的实体类,则返回 true。 -
setupAccess(SetupAttributeAccessEvent)
在这个方法中通过操作属性集合来设置访问权限。应该使用事件对象的addHidden()
、addReadOnly()
和addRequired()
方法填充只读、隐藏和必须属性的集合。可通过getEntity()
方法获得实体实例,实体实例处于托管状态,因此可以安全地访问其属性和其关联实体的属性。
-
例如,假设 Order
实体具有 customer
和 amount
属性,可以根据客户(customer)创建以下规则来限制对 amount
属性的访问:
package com.company.sample.core;
import com.company.sample.entity.Order;
import com.haulmont.cuba.core.app.SetupAttributeAccessHandler;
import com.haulmont.cuba.core.app.events.SetupAttributeAccessEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component("sample_OrderAttributeAccessHandler")
public class OrderAttributeAccessHandler implements SetupAttributeAccessHandler<Order> {
@Override
public boolean supports(Class clazz) {
return Order.class.isAssignableFrom(clazz);
}
@Override
public void setupAccess(SetupAttributeAccessEvent<Order> event) {
Order order = event.getEntity();
if (order.getCustomer() != null) {
if ("PLATINUM".equals(order.getCustomer().getGrade().getCode())) {
event.addHidden("amount");
} else if ("GOLD".equals(order.getCustomer().getGrade().getCode())) {
event.addReadOnly("amount");
}
}
}
}
- 通用 UI 中的属性访问控制
-
在发送BeforeShowEvent和AfterShowEvent事件之间,框架会自动在界面应用属性访问限制。如果不想在特定界面中使用,可以在界面控制器类添加
@DisableAttributeAccessControl
注解。可能希望在界面打开时重新计算并应用限制,以响应用户操作。您可以使用
AttributeAccessSupport
bean 来完成它,传递当前界面和状态已更改的实体。例如:@UiController("sales_Order.edit") @UiDescriptor("order-edit.xml") @EditedEntityContainer("orderDc") @LoadDataBeforeShow public class OrderEdit extends StandardEditor<Order> { @Inject private AttributeAccessSupport attributeAccessSupport; @Subscribe(id = "orderDc", target = Target.DATA_CONTAINER) protected void onOrderDcItemPropertyChange(InstanceContainer.ItemPropertyChangeEvent<Order> event) { if ("customer".equals(event.getProperty())) { attributeAccessSupport.applyAttributeAccess(this, true, getEditedEntity()); } } }
applyAttributeAccess()
方法的第二个参数是一个布尔值,它指定在应用新限制之前是否将组件访问权限重置为默认值。如果参数值为 true,则程序中对组件状态的更改(如果有)将丢失。在界面打开时自动调用该方法时,此参数的值为 false。但是在响应 UI 事件时调用该方法时,将其设置为 true,否则对组件的限制将被累加而不是替换。属性访问限制仅适用于绑定到单个实体属性的组件,如 TextField 或 LookupField。Table 和实现
ListComponent
接口的其它组件不受影响。因此,如果要编写可以隐藏多个实体实例的一个属性的规则,建议直接不要在表格中显示此属性。
3.3. 数据库组件
本节介绍如何配置应用程序以使用特定的 DBMS。同时描述了一种基于脚本的机制,该机制可以创建数据库,并在应用程序开发和上线运行后的整个周期中使其保持最新。
数据库组件属于中间件块(block); 应用程序的其它块(block)无法直接访问数据库。
有关使用数据库的一些其它信息,请参阅使用数据库部分。
3.3.1. DBMS 类型
应用程序中使用的 DBMS 的类型由cuba.dbmsType和cuba.dbmsVersion(可选)应用程序属性定义。这些属性会影响各种依赖于数据库类型的平台机制。
应用程序通过 javax.sql.DataSource
连接数据库,javax.sql.DataSource
是通过cuba.dataSourceJndiNamecuba.dataSourceJndiName中指定的名称(默认情况下是 java:comp/env/jdbc/CubaDS
)从 JNDI 中获取的。标准部署方式的数据源配置在 core 模块的 context.xml 文件中定义。数据源应使用适用于所选 DBMS 的 JDBC 驱动程序。
平台支持以下“开箱即用”的 DBMS 类型:
cuba.dbmsType | cuba.dbmsVersion | JDBC driver | |
---|---|---|---|
HSQLDB |
hsql |
org.hsqldb.jdbc.JDBCDriver |
|
PostgreSQL 8.4+ |
postgres |
org.postgresql.Driver |
|
Microsoft SQL Server 2005 |
mssql |
2005 |
net.sourceforge.jtds.jdbc.Driver |
Microsoft SQL Server 2008 |
mssql |
com.microsoft.sqlserver.jdbc.SQLServerDriver |
|
Microsoft SQL Server 2012+ |
mssql |
2012 |
com.microsoft.sqlserver.jdbc.SQLServerDriver |
Oracle Database 11g+ |
oracle |
oracle.jdbc.OracleDriver |
|
MySQL 5.6+ |
mysql |
com.mysql.jdbc.Driver |
下表描述了 Java 中的实体属性与不同 DBMS 中的表列之间推荐的数据类型映射关系。生成创建和更新数据库的脚本时,CUBA Studio 会自动使用这些类型。使用这些类型,可以保证所有平台机制正常运行。
Java | HSQL | PostgreSQL | MS SQL Server | Oracle | MySQL |
---|---|---|---|---|---|
UUID |
varchar(36) |
uuid |
uniqueidentifier |
varchar2(32) |
varchar(32) |
Date |
timestamp |
timestamp |
datetime |
timestamp |
datetime(3) |
java.sql.Date |
timestamp |
date |
datetime |
date |
date |
java.sql.Time |
timestamp |
time |
datetime |
timestamp |
time(3) |
BigDecimal |
decimal(p, s) |
decimal(p, s) |
decimal(p, s) |
number(p, s) |
decimal(p, s) |
Double |
double precision |
double precision |
double precision |
float |
double precision |
Long |
bigint |
bigint |
bigint |
number(19) |
bigint |
Integer |
integer |
integer |
integer |
integer |
integer |
Boolean |
boolean |
boolean |
tinyint |
char(1) |
boolean |
String (limited) |
varchar(n) |
varchar(n) |
varchar(n) |
varchar2(n) |
varchar(n) |
String (unlimited) |
longvarchar |
text |
varchar(max) |
clob |
longtext |
byte[] |
longvarbinary |
bytea |
image |
blob |
longblob |
通常,在数据库和 Java 代码之间转换数据的整个工作由ORM 层使用合适 JDBC 驱动程序来完成。这意味着使用EntityManager方法和JPQL 查询处理数据时不需要手动转换数据;对于开发人员来说,在编写与数据库交互的代码时应避免使用表格左栏没有列出的 Java 类型。
当通过 EntityManager.createNativeQuery() 或 QueryRunner 使用本地 SQL 时,Java 代码中的某些类型将与上面提到的类型不同,具体取决于所使用的 DBMS。特别是对于 UUID
类型的属性 - 只有 PostgreSQL 驱动程序使用此类型返回相应列的值; 其它数据库服务都返回 String
。要对不同的数据库类型抽象出通用应用程序代码,建议使用DbTypeConverter接口转换参数类型和查询结果。
3.3.1.1. 对其它 DBMS 的支持
在应用程序项目中,可以使用ORM框架(EclipseLink)支持的任何 DBMS。请按照以下步骤操作:
-
在 cuba.dbmsType 属性中以任意形式代码的指定数据库的类型。代码必须与平台中使用的代码不同:
hsql
、postgres
、mssql
、oracle
。 -
实现
DbmsFeatures
、SequenceSupport
、DbTypeConverter
接口,实现类用以下格式命名:<Type>DbmsFeatures
、<Type>SequenceSupport
和<Type>DbTypeConverter
,其中Type
是 DBMS 类型代码。实现类的包必须与接口的包相同。 -
在以 DBMS 类型代码命名的目录中创建数据库初始化和更新脚本。初始化脚本必须创建平台实体所需的所有数据库对象(可以从现有的
10-cuba
等目录中复制并修改这些脚本,使其适用于新的 DBMS 类型)。 -
要通过 Gradle 任务创建和更新数据库,需要在
build.gradle
中为这些任务指定额外的参数:task createDb(dependsOn: assemble, type: CubaDbCreation) { dbms = 'my' // DBMS code driver = 'net.my.jdbc.Driver' // JDBC driver class dbUrl = 'jdbc:my:myserver://192.168.47.45/mydb' // Database URL masterUrl = 'jdbc:my:myserver://192.168.47.45/master' // URL of a master DB to connect to for creating the application DB dropDbSql = 'drop database mydb;' // Drop database statement createDbSql = 'create database mydb;' // Create database statement timeStampType = 'datetime' // Date and time datatype - needed for SYS_DB_CHANGELOG table creation dbUser = 'sa' dbPassword = 'saPass1' } task updateDb(dependsOn: assemble, type: CubaDbUpdate) { dbms = 'my' // DBMS code driver = 'net.my.jdbc.Driver' // JDBC driver class dbUrl = 'jdbc:my:myserver://192.168.47.45/mydb' // Database URL dbUser = 'sa' dbPassword = 'saPass1' }
3.3.1.2. DBMS 版本
除了 cuba.dbmsType 应用程序属性外,还有一个可选的cuba.dbmsVersion属性。数据库版本属性影响对 DbmsFeatures
、SequenceSupport
、 DbTypeConverter
的接口实现的选择,同时也影响对数据库初始化和更新脚本的搜索。
这些集成接口实现类的名称结构如下:<Type><Version><Name>
。这里 Type
是 cuba.dbmsType
属性的值(大写),Version
是 cuba.dbmsVersion
的值,Name
是接口名称。类的包必须与接口的包一致。如果这个名称的类不可用,则会尝试查找名称中不带版本的类:<Type><Name>
。如果这样的类也不存在,则会抛出异常。
例如,在平台中定义了 com.haulmont.cuba.core.sys.persistence.Mssql2012SequenceSupport
类。如果在项目中指定了以下属性,则此类会生效:
cuba.dbmsType = mssql
cuba.dbmsVersion = 2012
对于数据库初始和更新脚本的搜索,<type>-<version>
目录优先于 type
目录。这意味着 <type>-<version>
目录中的脚本会代替 <type>
目录中具有相同名称的脚本。<type>-<version>
目录还可以包含一些具有唯一名称的脚本;它们也会被添加到公共的脚本集中以供执行。脚本按照路径排序,从 <type>
或 <type>-<version>
目录的第一个子目录开始,即不管脚本在哪个的目录(是否有版本)。
例如,对于 Microsoft SQL Server 2012 之前和之后版本的数据库,其初始化脚本应如下所示:
modules/core/db/init/
mssql/
10.create-db.sql
20.create-db.sql
30.create-db.sql
mssql-2012/
10.create-db.sql
3.3.2. 创建和更新数据库的脚本
CUBA 应用程序项目总是包含两组脚本(另请参阅创建数据库架构):
-
用于创建数据库的脚本,该脚本用于从零开始创建数据库。它们包含一组 DDL 和 DML 操作,这些操作会创建一个空数据库结构,该结构与应用程序数据模型的当前状态完全一致。这些脚本还可以使用必要的初始化数据填充数据库。
-
用于更新数据库的脚本,用于将数据库结构从任意一个旧的结构更新为与当前数据模型对应的结构。
更改数据模型时,必须通过 Create 和 Update 脚本使数据库结构同步相应的更改。例如,在将 address
属性添加到 Customer
实体时,有必要:
-
更改建表脚本:
create table SALES_CUSTOMER ( ID varchar(36) not null, CREATE_TS timestamp, CREATED_BY varchar(50), NAME varchar(100), ADDRESS varchar(200), -- added column primary key (ID) )
-
添加修改同一个表的更新脚本:
alter table SALES_CUSTOMER add ADDRESS varchar(200)
请注意,Studio 更新脚本生成器不会跟踪属性 Column definition 和自定义数据类型的
sqlType
的更改。因此,如果更改了它们,请手动创建相应的更新脚本。
创建的脚本位于 core 模块的 /db/init
目录下。对于应用程序支持的每种类型的 DBMS,都会创建一组单独的脚本,这些脚本位于cuba.dbmsType应用程序属性中指定的子目录中,例如 /db/init/postgres
。创建脚本的名称应该符合以下格式:{optional_prefix}create-db.sql
。
更新脚本位于 core 模块的 /db/update
目录中。对于应用程序支持的每种类型的 DBMS,都会创建一组单独的脚本,这些脚本位于cuba.dbmsType应用程序属性中指定的子目录中,例如 /db/update/postgres
。
更新脚本有两种类型:使用 *.sql
扩展名或 *.groovy
扩展名。更新数据库的主要方法是使用 SQL 脚本。Groovy 脚本仅由数据库脚本服务器执行机制执行,因此它们主要用于生产环境,在数据迁移或导入无法使用纯 SQL 实现时。如果要跳过 Groovy 更新脚本,可以在命令行中运行以下命令:
delete from sys_db_changelog where script_name like '%groovy' and create_ts > (now() - interval '1 hour')
更新脚本应具有特定格式的名称,更新机制会按脚本名称的字母顺序排序以形成正确的执行顺序(一般情况下,脚本名称根据创建时间来命名)。因此,在手动创建此类脚本时,建议使用以下格式指定更新脚本的名称: {yymmdd}-{description}.sql
,其中 yy
表示年份,mm
是表示月份,dd
表示日期,description
是脚本的简短描述。例如,121003-addCodeToCategoryAttribute.sql
。Studio 自动生成脚本时也遵循此格式。
要使用 updateDb
任务(gradle task)执行 groovy 更新脚本,groovy 脚本的扩展名应该是 .upgrade.groovy
, 并且遵循相同的命名规则。注意在此时,该脚本中不允许 Post Update 操作,相同的 ds
(访问数据源)和 log
(访问日志记录)变量需要用来做数据绑定。可以通过在 build.gradle
的 updateDb
任务中设置 executeGroovy = false
来禁用 groovy 脚本的执行。
可以将更新脚本分组到子目录中,但是,带有子目录的脚本路径不应该违背按时间排序的顺序。例如,可以使用年份或年份和月份创建子目录。
在已部署的应用程序中,用于创建和更新数据库的脚本位于特定的数据库脚本目录中,该目录由cuba.dbDir应用程序属性设置。
3.3.2.1. SQL 脚本的结构
用于创建和更新数据库的 SQL 脚本是一组由 “^” 字符分隔的 DDL 和 DML 命令组成的文本文件。这里使用了 “^” 字符,以便 “;” 分隔符可以用来分隔复杂的命令;例如,在创建函数或触发器时。脚本执行机制使用 “^” 分隔符将输入文件拆分为单独的命令,并在独立的事务中执行每个命令。这意味着如果需要的话可以将几条单独的语句(例如,insert
)组合在一起,用分号分隔,确保它们在同一个事务中执行。
“^” 分隔符可以通过使用两个 “^” 来转义。例如,如果要将 |
SQL 格式的更新脚本示例:
create table LIBRARY_COUNTRY (
ID varchar(36) not null,
CREATE_TS time,
CREATED_BY varchar(50),
NAME varchar(100) not null,
primary key (ID)
)^
alter table LIBRARY_TOWN add column COUNTRY_ID varchar(36) ^
alter table LIBRARY_TOWN add constraint FK_LIBRARY_TOWN_COUNTRY_ID foreign key (COUNTRY_ID) references LIBRARY_COUNTRY(ID)^
create index IDX_LIBRARY_TOWN_COUNTRY on LIBRARY_TOWN (COUNTRY_ID)^
3.3.2.2. Groovy 脚本的结构
Groovy 更新脚本的结构如下:
-
主要(main) 部分,包含要在应用程序上下文启动之前执行的代码。在这一部分,可以使用任何 Java、Groovy 和 Middleware 应用程序块(block)中的类。但要注意,这时尚未实例化 bean、基础设施接口和其它应用程序对象,所以无法使用它们。
这部分主要用于更新数据库结构,通常使用普通的 SQL 脚本。
-
PostUpdate 部分 - 一组闭包(Groovy 中的概念),将在应用程序上下文启动后和更新过程完成后执行。在这些闭包中,可以使用任何中间件对象。
在脚本的这一部分中,可以比较方便地执行数据导入,因为可以使用Persistence接口和数据模型对象。
执行机制将以下变量传递给 Groovy 脚本:
-
ds
– 用于应用程序数据库的javax.sql.DataSource
实例; -
log
–org.apache.commons.logging.Log
实例,用于在服务端日志中输出信息; -
postUpdate
– 包含add(Closure closure)
方法的对象,用于添加上述 PostUpdate 闭包。
Groovy 脚本仅由执行数据库脚本的服务端机制执行。 |
Groovy 更新脚本的示例:
import com.haulmont.cuba.core.Persistence
import com.haulmont.cuba.core.global.AppBeans
import com.haulmont.refapp.core.entity.Colour
import groovy.sql.Sql
log.info('Executing actions in update phase')
Sql sql = new Sql(ds)
sql.execute """ alter table MY_COLOR add DESCRIPTION varchar(100); """
// Add post update action
postUpdate.add({
log.info('Executing post update action using fully functioning server')
def p = AppBeans.get(Persistence.class)
def tr = p.createTransaction()
try {
def em = p.getEntityManager()
Colour c = new Colour()
c.name = 'yellow'
c.description = 'a description'
em.persist(c)
tr.commit()
} finally {
tr.end()
}
})
3.3.3. Gradle 任务执行数据库脚本
应用程序开发人员通常使用此机制来更新自己的数据库实例。脚本的执行本质上是通过build.gradle构建脚本运行特定的 Gradle 任务。这个操作可以通过命令行或 Studio 界面完成。
要运行脚本创建数据库,需使用 createDb
任务。在 Studio 中,它对应于主菜单中的 CUBA > Create database 命令。启动此任务时,会执行以下操作:
-
当前项目的应用程序组件的脚本和 core 模块的
db/**/*.sql
脚本被构建到modules/core/build/db
目录中。应用程序组件的脚本集位于有数字前缀的子目录中。前缀用于根据组件之间的依赖关系提供脚本执行的字母排序。 -
如果数据库存在,它会被完全清除。一个新的空数据库会被创建。
-
modules/core/build/db/init/**/*create-db.sql
子目录中的所有创建脚本按字母顺序依次执行,并且它们的名称以及相对于 db 目录的路径会注册在 SYS_DB_CHANGELOG 表中。 -
类似地,所有当前可用的
modules/core/build/db/update/**/*.sql
更新脚本会注册在 SYS_DB_CHANGELOG 表中。这对于后续进行数据库增量更新是必须的。
要运行脚本更新数据库,需使用 updateDb
任务。在 Studio 中,它对应于主菜单中的 CUBA > Update database 命令。启动此任务时,会执行以下操作:
-
脚本的构建方式与上述
createDb
命令的构建方式相同。 -
执行机制检查是否已运行应用程序组件的所有创建脚本(通过检查
SYS_DB_CHANGELOG
表)。如果没有,则执行应用程序组件创建脚本并在SYS_DB_CHANGELOG
表中注册。 -
在
modules/core/build/db/update/**
目录中搜索未在SYS_DB_CHANGELOG
表中注册的更新脚本,即以前没有执行过的更新脚本。 -
对上一步中找到的所有脚本按字母顺序依次执行,同时脚本的名称以及相对于
db
目录的路径都会注册到SYS_DB_CHANGELOG
表中。
3.3.4. 在 web Server 中执行数据库脚本
Web Server 执行数据库脚本的机制用于更新数据库,这个操作在应用程序服务启动、中间件块(block)初始化期间激活。显然,应用程序应该已经构建并部署在 Web Server 上,即在生产环境或开发人员的 Tomcat 实例中。
根据下面描述的条件,该机制执行创建或更新脚本,也就是它可以从头开始初始化 DB 并对其进行更新。但是,与上一节中描述的 Gradle createDb
任务不同,数据库必须存在才能初始化 - 在 Web Server 中不会自动创建 DB,而只是执行脚本。
Web Server 中执行脚本的机制如下:
-
这些脚本是从数据库脚本目录中提取的,该目录由cuba.dbDir应用程序属性定义,该属性的默认设置为
tomcat/webapps/app-core/WEB-INF/db
。 -
如果数据库没有
SEC_USER
表,则被视为空数据库,会使用创建脚本运行完整初始化过程。执行初始化脚本后,这些脚本的名称会存储在SYS_DB_CHANGELOG
表中。所有可用的更新脚本的名称也存储在同一个表中,但是并没有执行过这些更新脚本。 -
如果数据库有
SEC_USER
表,但没有SYS_DB_CHANGELOG
表(当在现有生产环境数据库上首次启动这里描述的机制时就会出现这种情况),这种情况下不执行任何脚本,而是创建SYS_DB_CHANGELOG
表,并存储所有当前可用的创建和更新脚本的名称。 -
如果数据库同时具有
SEC_USER
和SYS_DB_CHANGELOG
表,则执行先前未将名称存储在SYS_DB_CHANGELOG
表中的更新脚本,然后将这些脚本名称存储到SYS_DB_CHANGELOG
表中。脚本执行的顺序由两个因素决定:应用程序组件的优先级(参阅数据库脚本目录:10-cuba
、20-bpm
,…)和按字母顺序排序的脚本文件名称(参考update
目录的子目录)。在执行更新脚本之前,先检查是否已运行应用程序组件的所有创建脚本(通过检查
SYS_DB_CHANGELOG
表)。如果某个应用程序组件使用的数据库未初始化,则会执行其创建脚本。
通过cuba.automaticDatabaseUpdate应用程序属性启用在服务器启动时执行脚本的机制。
在运行中的应用程序中,可以使用 update
作为参数调用 app-core.cuba:type=PersistenceManager
JMX bean 的 updateDatabase()
方法来启动脚本执行机制。显然,这种方式只能更新现有的 DB,因为无法登录只有空 DB 的系统来运行 JMX bean 方法。 请注意,如果部分在中间件启动或用户登录期间初始化数据模型与数据架构不匹配的话,会发生不可恢复的错误。这就是一般都在服务器启动时数据模型初始化前执行数据库自动更新的原因。
JMX app-core.cuba:type=PersistenceManager
bean 还有一个与 DB 更新机制相关的方法: findUpdateDatabaseScripts()
。它返回目录中可用的但未在 DB 中注册(尚未执行)新的更新脚本列表。
有关使用服务器数据库更新机制的建议,请参阅在生产环境中创建和更新数据库。
3.4. 中间件组件
下图显示了 CUBA 应用程序中间件的主要组件。
服务(Service)是容器托管组件,用于形成应用边界并为客户端层提供接口。服务自身可以包含业务逻辑,也可以将业务逻辑的实现委托给托管 bean。
Persistence是一个基础设施接口,用于访问数据存储功能:ORM和事务管理。
3.4.1. 服务
下图展示了服务层组件的类关系:
服务接口位于 global 模块中,所以在中间层和客户端层都是可用的。在运行时,在客户端层会为服务接口创建代理。代理使用 Spring HTTP Invoker 机制提供服务 bean 方法的调用。
服务实现 bean 位于 core 模块,仅在中间层可用。
使用 Spring AOP 的任何服务方法都会自动调用 ServiceInterceptor
。它检测当前线程中用户会话的可用性,并且会在从客户端层调用服务时执行转换和记录异常。
3.4.1.1. 创建服务
服务接口的名称应以 Service
结尾,实现类的名称应以 ServiceBean
结尾。
CUBA Studio 能帮助轻松创建服务接口的脚手架代码和存根类。Studio 还会自动在 spring.xml
中注册新服务。要创建服务,请使用 CUBA 项目树的 Middleware 节点中的 New>Service 任务。
如果要手动创建服务,请按照以下步骤操作。
-
在 global 模块中创建服务接口(因为服务接口必须在所有层中可用),并在其中指定服务名称。建议使用以下格式指定名称:
{project_name}_{interface_name}
。例如:package com.sample.sales.core; import com.sample.sales.entity.Order; public interface OrderService { String NAME = "sales_OrderService"; void calculateTotals(Order order); }
-
在 core 模块中创建服务类,并使用接口中指定的名称向其添加
@org.springframework.stereotype.Service
注解:package com.sample.sales.core; import com.sample.sales.entity.Order; import org.springframework.stereotype.Service; @Service(OrderService.NAME) public class OrderServiceBean implements OrderService { @Override public void calculateTotals(Order order) { } }
作为托管 bean 的服务类应放在包的树结构中,其根目录需要在spring.xml文件的 context:component-scan
元素中指定。这时,spring.xml
文件会包含以下元素:
<context:component-scan base-package="com.sample.sales"/>
这意味着将从 com.sample.sales
包开始搜索此应用程序 block 中带注解的 bean。
如果不同的服务或其它中间件组件需要调用相同的业务逻辑,则应将其提取并封装在适当的托管 Bean中。例如:
// service interface
public interface SalesService {
String NAME = "sample_SalesService";
BigDecimal calculateSales(UUID customerId);
}
// service implementation
@Service(SalesService.NAME)
public class SalesServiceBean implements SalesService {
@Inject
private SalesCalculator salesCalculator;
@Transactional
@Override
public BigDecimal calculateSales(UUID customerId) {
return salesCalculator.calculateSales(customerId);
}
}
// managed bean encapsulating business logic
@Component
public class SalesCalculator {
@Inject
private Persistence persistence;
public BigDecimal calculateSales(UUID customerId) {
Query query = persistence.getEntityManager().createQuery(
"select sum(o.amount) from sample$Order o where o.customer.id = :customerId");
query.setParameter("customerId", customerId);
return (BigDecimal) query.getFirstResult();
}
}
3.4.1.2. 使用服务
为了调用服务,应该在应用程序的客户端 block 中创建相应的代理对象。各个 block 中都有一个特殊的工厂可以创建服务代理,Web 客户端:WebRemoteProxyBeanCreator
、Web 门户:PortalRemoteProxyBeanCreator
。
代理对象工厂在相应客户端 block 的spring.xml中配置,并包含服务名称和接口。
例如,要从 sales 应用程序中的 Web 客户端调用 sales_OrderService
服务,请将以下代码添加到 web 模块的 web-spring.xml
文件中:
<bean id="sales_proxyCreator" class="com.haulmont.cuba.web.sys.remoting.WebRemoteProxyBeanCreator">
<property name="serverSelector" ref="cuba_ServerSelector"/>
<property name="remoteServices">
<map>
<entry key="sales_OrderService" value="com.sample.sales.core.OrderService"/>
</map>
</property>
</bean>
所有导入的服务都应该在 remoteServices
属性中使用 map/entry
元素声明。
CUBA Studio 自动在项目的所有客户端 block 中注册服务。 |
从应用程序代码的角度来看,客户端级别的服务代理对象是标准的 Spring bean,可以通过注入或通过 AppBeans
类获得。例如:
@Inject
private OrderService orderService;
public void calculateTotals() {
orderService.calculateTotals(order);
}
或者
public void calculateTotals() {
AppBeans.get(OrderService.class).calculateTotals(order);
}
3.4.1.3. 数据服务
DataService
使用外观设计模式提供了从客户端层调用DataManager中间件实现的功能。建议不要在应用程序代码中使用 DataService
接口,应该直接在中间层和客户端层使用 DataManager
。
3.4.2. 数据存储
在 CUBA 应用程序中处理数据的常用方法是操作实体 - 可通过数据源和具有数据感知功能的可视化组件进行声明式处理,也可通过 DataManager 或 EntityManager 进行编程式处理。实体映射到数据存储中的数据,数据存储通常是关系型数据库。应用程序可以连接到多个数据存储,因此其数据模型将包含映射到位于不同数据库中的数据的实体。
实体只能属于单个数据存储,但是可以在单个 UI 界面上显示来自不同数据存储的实体,DataManager
可以确保在保存时将实体分派到适当的数据存储中去。根据实体类型,DataManager
选择一个已注册的数据存储,这个数据存储实现了 DataStore
接口,然后委托其加载和保存实体。当以编程的方式控制事务并通过 EntityManager
使用实体时,必须明确指定要使用的数据存储。有关详细信息,请参阅 Persistence 接口方法和 @Transactional 注解参数。
平台提供了 DataStore
接口的单一实现,名称为 RdbmsStore
,这个实现的目的是通过 ORM 层来使用关系型数据库。可以在自己的项目中实现 DataStore
接口以进行数据整合,例如,可以与非关系型数据库或具有 REST 接口的外部系统进行数据整合。
在任何 CUBA 应用程序中,必定存在一个主数据存储,它包含系统实体和安全实体,用户登录也是在主数据存储。在本手册中提及数据库时,如果没有明确说明,则指的是主数据存储。主数据存储必须是通过 JDBC 数据源连接的关系型数据库。主数据源位于 JNDI 中,其名称应在 cuba.dataSourceJndiName 应用程序属性中指定,默认情况下为 jdbc/CubaDS
。
可以在 cuba.additionalStores 应用程序属性中指定其它附加数据存储名称。如果附加存储是 RdbmsStore
,需要提供以下属性:
-
cuba.dataSourceJndiName_{store_name}
- 相应 JDBC 数据源的 JNDI 名称。 -
cuba.dbmsType_{store_name}
- 数据存储 DBMS 的类型。 -
cuba.persistenceConfig_{store_name}
- 数据存储persistence.xml
文件的位置。
如果在项目中为附加存储实现了 DataStore
接口,需要在 cuba.storeImpl_{store_name}
应用程序属性中指定实现 bean 的名称。
例如,如果需要使用另外两个数据存储:db1
(PostgreSQL 数据库)和 mem1
(由某个项目 bean 实现的内存存储),请在 core 模块的 app.properties
中指定以下应用程序属性:
cuba.additionalStores = db1, mem1
cuba.dataSourceJndiName_db1 = jdbc/db1
cuba.dbmsType_db1 = postgres
cuba.persistenceConfig_db1 = com/company/sample/db1-persistence.xml
cuba.storeImpl_mem1 = sample_InMemoryStore
还应在所有应用程序 block 使用的属性文件(web-app.properties
、 portal-app.properties
等)中指定 cuba.additionalStores
和 cuba.persistenceConfig_db1
属性。
CUBA Studio 允许在 CUBA Project Properties 窗口的 Data Stores 标签页上设置其它数据存储。它会自动创建所有必须的应用程序属性和 JDBC 数据源,同时维护额外的 |
- 来自不同数据存储的实体之间的引用
-
如果正确定义了 DataManager,则可以自动维护来自不同数据存储的实体之间的 TO-ONE 引用。比如,在主数据存储中有
Order
实体,在附加数据存储中有Customer
实体,并且希望在Order
中引用Customer
。可以这样做:-
在
Order
实体中,定义一个存储Customer
实体 ID 的属性。该属性应使用@SystemLevel
注解,以将其从用户可用的各种列表中排除,例如 Filter 中的可选属性:@SystemLevel @Column(name = "CUSTOMER_ID") private Long customerId;
-
在
Order
实体中,定义对Customer
的非持久引用,并将 "related" 指定为customerId
:@Transient @MetaProperty(related = "customerId") private Customer customer;
-
在适当的视图中包含非持久化的
customer
属性。
之后,当使用包含
customer
属性的视图加载Order
时,DataManager
会自动从附加数据存储加载关联的Customer
。集合的加载针对性能进行了优化:在加载 Order 列表之后,从附加数据存储加载引用的 Customer 是分批完成的。每批加载的记录数由 cuba.crossDataStoreReferenceLoadingBatchSize 应用程序属性定义。当提交引用了
Customer
的Order
实体图时,DataManager
会通过相应的DataStore
进行数据保存,然后将 Customer 的 ID 保存在 Order 的customerId
属性中。Filter 组件也支持跨数据存储引用。
在 Studio 中,如果使用了跨数据存储的实体引用属性,Studio 会自动维护这种引用关系。
-
3.4.3. 持久化接口
Persistence
接口是ORM层数据存储功能的入口。
该接口有以下方法:
-
createTransaction()
、getTransaction()
– 获取管理事务的接口。该方法可以接受一个数据存储名称作为参数。如果不指定数据存储名称,则使用主数据存储。 -
callInTransaction()
、runInTransaction()
- 在新的事务中执行指定操作,操作可以有返回值,也可以没有。该方法可以接受一个数据存储名称作为参数。如果不指定数据存储,则使用主数据存储。 -
isInTransaction()
– 检查当前是否有活动的事务。 -
getEntityManager()
– 返回绑定到当前事务的EntityManager实例。该方法可以接受一个数据存储名称作为参数。如果不指定数据存储,则使用主数据存储。 -
isSoftDeletion()
– 检查是否启用了软删除模式。 -
setSoftDeletion()
– 启用或禁用软删除模式。设置此属性会影响所有新创建的EntityManager
实例。默认启用软删除。 -
getDbTypeConverter()
– 返回主数据存储或其它数据存储的DbTypeConverter实例。 -
getDataSource()
– 返回主数据存储或附加数据存储的javax.sql.DataSource
实例。对于通过
getDataSource().getConnection()
方法获得的所有javax.sql.Connection
对象,在使用连接后,应在finally
中调用close()
方法。否则,连接不会被重新放回连接池。随着时间的推移连接池将溢出,应用程序将无法执行数据库查询。 -
getTools()
– 返回PersistenceTools
接口的实例(见下文)。
3.4.3.1. PersistenceTools
托管 Bean,包含了与数据存储功能相关的辅助方法。可以通过调用 Persistence.getTools()
方法获得,或者像任何其它 bean 一样,通过注入或 AppBeans
类来获得。
PersistenceTools
bean 有以下方法:
-
getDirtyFields()
– 返回自最后一次从数据库加载实例以来已更改的实体属性的名称集合,对于新实例,返回空集合。 -
isLoaded()
– 检查是否从数据库加载了指定的实例属性。如果在加载实例时指定的视图中不存在该属性,则可能没有加载该属性。此方法仅适用于托管状态的实例.
-
getReferenceId()
– 返回关联实体的 ID 而不需要从数据库加载关联实体的数据。假设在持久化上下文中加载了一个
Order
实体并且需要获得这个Order
关联的Customer
实例的 ID 值。如果调用order.getCustomer().getId()
方法,将执行数据库查询来加载Customer
实例,但此时这个数据库查询是没必要的,因为 Customer ID 的值作为外键也存在于Order
表中。而执行persistence.getTools().getReferenceId(order, "customer")
则不会向数据库发送任何其它查询。
此方法仅适用于托管状态的实例。
在应用程序中,可以通过重写 PersistenceTools
bea 来扩展默认的辅助方法的集合。使用扩展接口的示例如下所示:
MyPersistenceTools tools = persistence.getTools();
tools.foo();
((MyPersistenceTools) persistence.getTools()).foo();
3.4.3.2. DbTypeConverter
该接口包含了在数据模型属性值和 JDBC 查询的参数/结果之间的进行转换的方法。通过Persistence.getDbTypeConverter()方法可以获得此接口的实例对象。
DbTypeConverter
接口有以下方法:
-
getJavaObject()
– 将 JDBC 查询的结果转换成为适合分配给实体属性的类型。 -
getSqlObject()
– 将实体属性的值转换为适合分配给 JDBC 查询参数的类型。 -
getSqlType()
– 返回与传递的实体属性类型相应的java.sql.Types
常量
3.4.4. ORM 层
对象关系映射(ORM)是一种将关系型数据库表映射到编程语言对象的技术。CUBA 使用基于 EclipseLink 框架的 ORM 实现。
ORM 提供一些明显的优点:
-
通过操控 Java 对象来操控关系型 DBMS。
-
通过消除枯燥的 SQL 查询语句的编写来简化编程。
-
通过一个命令来加载和保存整个对象图来简化编程。
-
允许将应用程序轻松地移植到不同的 DBMS。
-
允许使用简洁的对象查询语言 – JPQL。
同时,ORM 也存在一些缺点。首先,直接使用 ORM 的开发者需要对它有很深的了解,知道它是如何工作的。此外,由于使用了 ORM,使得 SQL 的直接优化和使用 DBMS 的特性变得困难。
如果对数据库访问出现任何性能问题,首先应检查的是实际执行的 SQL 语句。可以使用 |
3.4.4.1. EntityManager
EntityManager
- 用于处理持久化实体的主要 ORM 接口。
有关 EntityManager 和 DataManager 之间差异的信息,请参阅 DataManager 与 EntityManager。 |
可以通过调用 Persistence 接口的 getEntityManager()
方法获取 EntityManager
的引用。获取到的 EntityManager
实例绑定到当前事务,在一个事务中对 getEntityManager()
方法的所有调用都将返回同一个 EntityManager
实例。在事务结束后便不能再使用此事务的 EntityManager
实例。
EntityManager
的实例包含 持久化上下文 ,持久化上下文存储了从数据库加载的或新创建的一组实体实例。持久化上下文是处于事务中的数据缓存。EntityManager
会自动将持久化上下文中所做的所有更改在事务提交时或者调用 EntityManager.flush()
方法时更新到数据库。
CUBA 应用程序中使用的 EntityManager
接口主要复制标准 javax.persistence.EntityManager 接口。下面是它的主要方法:
-
persist()
– 将实体的新实例添加到持久化上下文中。提交事务时,会使用INSERT
SQL 语句在 DB 中创建相应的记录。 -
merge()
– 通过以下方式将游离实例的状态复制到持久化上下文:从 DB 中加载相同标识符的实例,并将传递的游离实例的状态复制到这个加载的实例,然后返回加载的托管实例。之后,应该使用返回的托管实例。在事务提交时,会使用UPDATE
SQL 语句将该实体的状态存储到 DB 中。 -
remove()
– 从数据库中删除对象,或者,如果启用了软删除模式,则只设置deleteTs
和deletedBy
属性。如果传递的实例处于游离状态,则首先执行
merge()
方法。 -
find()
– 通过标识符加载实体实例。当向数据库发送请求时,系统会将传递的视图作为参数传递给该方法。因此,持久化上下文将包含加载了所有视图属性的对象图。如果没有传递视图,则默认使用
_local
视图。 -
createQuery()
– 创建一个Query
或TypedQuery
对象来执行JPQL 查询。 -
createNativeQuery()
– 创建一个Query
对象来执行SQL 查询。 -
reload()
– 使用提供的视图重新加载实体实例。 -
isSoftDeletion()
– 检查EntityManager
是否处于软删除模式。 -
setSoftDeletion()
– 为EntityManager
设置软删除模式。 -
getConnection()
– 返回当前事务对应的连接。这种连接不需要主动关闭,它会在事务完成时自动关闭。 -
getDelegate()
– 返回 ORM 实现提供的javax.persistence.EntityManager
。
在服务中使用 EntityManager
的示例:
@Service(SalesService.NAME)
public class SalesServiceBean implements SalesService {
@Inject
private Persistence persistence;
@Override
public BigDecimal calculateSales(UUID customerId) {
BigDecimal result;
// start transaction
try (Transaction tx = persistence.createTransaction()) {
// get EntityManager for the current transaction
EntityManager em = persistence.getEntityManager();
// create and execute Query
Query query = em.createQuery(
"select sum(o.amount) from sample$Order o where o.customer.id = :customerId");
query.setParameter("customerId", customerId);
result = (BigDecimal) query.getFirstResult();
// commit transaction
tx.commit();
}
return result != null ? result : BigDecimal.ZERO;
}
}
- 部分实体
-
默认情况下,在 EntityManager 中,视图仅影响引用属性,本地属性会被全部加载。
如果将视图的loadPartialEntities属性设置为 true,则可以强制 EntityManager 加载 部分(partial) 实体(像DataManager所做的一样)。但是,如果加载的实体是缓存的(cached),则忽略此视图属性,仍将加载实体的所有本地属性。
3.4.4.2. 实体状态
- New(新建状态)
-
刚在内存中创建的实例:
Car car = new Car()
。可以将新实例传递给
EntityManager.persist()
以存储到数据库,在这种情况下,会将其状态更改为 Managed。 - Managed(托管状态)
-
从数据库加载的实例,或传递给
EntityManager.persist()
的新实例。这个实例属于EntityManager
实例,即包含在其持久化上下文中。当提交
EntityManager
所属的事务时,托管实例的任何更改都将保存到数据库中。 - Detached(游离状态)
-
从数据库加载并与其持久化上下文分离的实例(事务结束或实体实例通过序列化产生)。
只有通
EntityManager.merge()
方法将此实例变成托管状态时,应用到游离实例的更改才能保存到数据库中。
3.4.4.3. 延迟加载
延迟加载(按需加载,也称懒加载)启用关联实体的延迟加载,即在第一次访问其属性时加载它们。
延迟加载会比贪婪加载生成更多的数据库查询,但是这些查询会在时间维度上推迟。
-
例如,在延迟加载实体 A 的 N 个实例的列表的情况下,每个实例包含到实体 B 的实例的链接,将需要对 DB 进行 N+1 个请求,1 个请求用来加载 N 个 A,N 个请求用来为每个 A 加载 B。
-
在大多数情况下,最小化对数据库的请求数会使响应时间和数据库负担减少。平台使用视图的机制来实现这一目标。使用视图允许 ORM 仅为上述关联表的情况执行一次数据库请求。
延迟加载仅适用于托管状态的实例,即在加载实例的事务中。
3.4.4.4. 执行 JPQL 查询
Query
接口用于执行 JPQL 查询。可以通过调用 createQuery()
方法从当前的 EntityManager
实例获得其引用。如果该查询用于加载实体,建议使用结果类型作为参数调用 createQuery()
。这将创建一个 TypedQuery
实例。
Query
的方法主要对应于标准 JPA javax.persistence.Query 接口的方法。但有以下差异:
-
setParameter()
– 为查询参数设置值。如果该值是实体实例,则隐式将实例转换为它的标识符。例如:Customer customer = ...; TypedQuery<Order> query = entityManager.createQuery( "select o from sales$Order o where o.customer.id = ?1", Order.class); query.setParameter(1, customer);
请注意,虽然参数中传递了一个实体实例,但是查询中的比较使用的是实例的标识符。
使用了
implicitConversions = false
的重载方法不执行此类转换。 -
setView()
、addView()
– 定义用于加载数据的视图。 -
getDelegate()
– 返回由 ORM 实现提供的javax.persistence.Query
实例。
如果为查询指定了视图 ,则默认情况下查询使用 FlushModeType.AUTO
模式,这会影响当前持久化上下文包含已更改实体实例的情况:这些实例将在执行查询之前保存到数据库中。换句话说,ORM 首先同步持久化上下文和数据库中的实体状态,之后才执行查询。这样可以保证查询结果包含所有相关实例,即使它们尚未明确地保存到数据库中。这样做的缺点是会有一个隐式刷新(flush),也就是为所有当前已更改的实体实例执行 SQL 更新语句,这可能会影响性能。
如果在没有视图的情况下执行查询,则默认情况下查询使用 FlushModeType.COMMIT
模式,这意味着查询不会导致刷新(flush),并且查询结果将不会体现出当前持久化上下文数据。
在大多数情况下,忽略当前的持久化上下文是可以接受的,并且是首选行为,因为它不会导致额外的 SQL 更新。但是在使用视图时存在以下问题:如果持久化上下文中存在已更改的实体实例,并且使用视图以 FlushModeType.COMMIT
模式执行查询去加载相同的实例,则更改将丢失。这就是在运行带有视图的查询时默认使用 FlushModeType.AUTO
的原因。
还可以使用 Query
接口的 setFlushMode()
方法显式设置刷新(flush)模式,这样将覆盖上述默认设置。
3.4.4.4.1. JPQL 函数
CUBA 和标准 JPA 之间 JPQL 的主要区别在于 CUBA 函数总是需要一个实体别名,包括 SELECT
和 UPDATE/DELETE
语句。
CUBA | JPA |
---|---|
|
|
在开启实体的软删除模式时,如果对已经软删除的实体执行 JPQL DELETE FROM
语句将抛出异常。这样的语句实际上转换为 SQL 来删除那些没有标记为删除的所有实例。默认情况下禁用软删除,可以使用 cuba.enableDeleteStatementInSoftDeleteMode 应用程序属性启用软删除。
下表描述了 CUBA 框架支持和不支持的 JPQL 函数。
函数 | 支持 | 查询 |
---|---|---|
聚合函数 |
支持 |
|
不支持: 带标量表达式的聚合函数(EclipseLink 特性) |
|
|
ALL、 ANY、 SOME |
支持 |
|
算术函数 (INDEX 、 SIZE 、 ABS 、 SQRT 、 MOD) |
支持 |
|
UPDATE 查询中的 CASE 表达式 |
支持 |
|
不支持: UPDATE 查询中的 CASE |
|
|
日期 函数(CURRENT_DATE、CURRENT_TIME、CURRENT_TIMESTAMP) |
支持 |
|
EclipseLink 函数 (CAST、 REGEXP、 EXTRACT) |
支持 |
|
不支持: GROUP BY 子句中的 CAST |
|
|
实体类型表达式 |
支持: 实体类型作为参数 |
|
不支持: 直接链接到实体类型 |
|
|
函数调用 |
支持: 比较子句中使用函数结果 |
|
不支持: 直接使用函数返回值 |
|
|
IN |
支持 |
|
IS EMPTY 集合 |
支持 |
|
键/值 KEY/VALUE |
不支持 |
|
字面量 |
支持 |
|
不支持: 时间和日期字符串 |
|
|
MEMBER OF |
支持: 字段或者查询结果 |
|
不支持: 字面量 |
|
|
SELECT 中使用 NEW |
支持 |
|
NULLIF/COALESCE |
支持 |
|
order by 中使用 NULLS FIRST, NULLS LAST |
支持 |
|
字符串函数 (CONCAT、 SUBSTRING 、 TRIM 、 LOWER 、 UPPER 、 LENGTH 、 LOCATE) |
支持 |
|
不支持: 带特定字符的 TRIM |
|
|
子查询 |
支持 |
|
不支持: 子查询语句 FROM 中使用路径表达式而不是实体名称 |
|
|
TREAT |
支持 |
|
不支持: WHERE 从句中使用 TREAT |
|
3.4.4.4.2. 不区分大小写的子串搜索
可以在查询参数的值中使用 (?i)
前缀来简单地指定忽略大小写的子串(Substring)搜索。例如这条查询语句:
select c from sales$Customer c where c.name like :name
如果传递字符串 (?i)%doe%
作为 name
参数的值,则查询将返回 John Doe
,如果数据库中存在此类记录,即使字符的大小写不同。出现这种结果是因为 ORM 将执行条件为 lower(C.NAME) like ?
的 SQL 查询。
请注意,这样的查询不能使用在此字段上的索引,即使数据库中已对该字段建立了索引。
3.4.4.4.3. JPQL 中的宏
JPQL 查询文本可以包含宏,这些宏在执行查询之前执行会被转换为可执行的 JPQL,并且还可以修改查询参数集。
宏解决了以下问题:
-
JPQL 有一个限制,这个限制导致条件中不能依赖给定的当前时间字段,(即像“current_date -1”这样的表达式不起作用),宏为这个限制提供一个解决方法。
-
能够将
Timestamp
类型字段(日期/时间字段)与日期进行比较。
下面是更多细节:
- @between
-
格式为
@between(field_name, moment1, moment2, time_unit)
或@between(field_name, moment1, moment2, time_unit, user_timezone)
,其中-
field_name
是要比较的属性的名称。 -
moment1
、moment2
– 开始时间点、结束时间点,field_name
的值在这两个时间点之间。时间点应该使用一个表达式定义,这个表达式包含了now
变量与整数的加减运算。 -
time_unit
– 定义在时间点表达式中now
中增加或减去的时间间隔的单位和时间点精度。下面是可能用到的值:year
、month
、day
、hour
、minute
、second
。 -
user_timezone
- 一个可选参数,用于定义在查询中要使用的当前用户时区。
宏在 JPQL 中转换为以下表达式:
field_name >= :moment1 and field_name < :moment2
例 1. 查询今天创建的 Customer:
select c from sales$Customer where @between(c.createTs, now, now+1, day)
例 2. 查询过去 10 分钟内创建的 Customer:
select c from sales$Customer where @between(c.createTs, now-10, now, minute)
例 3. 查询过去 5 天内的文件,考虑当前用户时区:
select d from sales$Doc where @between(d.createTs, now-5, now, day, user_timezone)
-
- @today
-
格式为
@today(field_name)
或@today(field_name, user_timezone)
,帮助定义检查属性值是否属于当天条件。从本质上讲,这是@between
宏的一个特例。例:查询今天创建的 Customer:
select d from sales$Doc where @today(d.createTs)
- @dateEquals
-
格式为
@dateEquals(field_name, parameter)
或@dateEquals(field_name, parameter, user_timezone)
,允许定义一个检查field_name
(Timestamp
格式)是否落入parameter
传递的日期范围的条件。例如:
select d from sales$Doc where @dateEquals(d.createTs, :param)
可以使用
now
属性来传入当前日期。如果需要设置日期偏移量,则可以将now
与+
或者-
一起使用,示例:select d from sales$Doc where @dateEquals(d.createTs, now-1)
- @dateBefore
-
格式为
@dateBefore(field_name, parameter)
或@dateBefore(field_name, parameter, user_timezone)
,允许定义一个条件检查field_name
值(Timestamp
格式)小于parameter
传递的日期。例如:
select d from sales$Doc where @dateBefore(d.createTs, :param, user_timezone)
可以使用
now
属性来传入当前日期。如果需要设置日期偏移量,则可以将now
与+
或者-
一起使用,示例:select d from sales$Doc where @dateBefore(d.createTs, now+1)
- @dateAfter
-
格式为
@dateAfter(field_name, parameter)
或@dateAfter(field_name, parameter, user_timezone)
,允许定义条件,即field_name
值的日期 (Timestamp
格式)大于或等于parameter
传递的日期。例如:
select d from sales$Doc where @dateAfter(d.createTs, :param)
可以使用
now
属性来传入当前日期。如果需要设置日期偏移量,则可以将now
与+
或者-
一起使用,示例:select d from sales$Doc where @dateAfter(d.createTs, now-1)
- @enum
-
允许使用完全限定的枚举常量名称而不是其数据库标识符。这可以简化在整个应用程序代码中搜索枚举用例的过程。
例如:
select r from sec$Role where r.type = @enum(com.haulmont.cuba.security.entity.RoleType.SUPER) order by r.name
3.4.4.5. 执行 SQL 查询
ORM 允许执行返回个别字段列表或实体实例的 SQL 查询。为此,通过调用 EntityManager.createNativeQuery()
方法创建 Query
或 TypedQuery
对象。
如果选择了个别列,则结果列表中每行的类型为 Object[]
。例如:
Query query = persistence.getEntityManager().createNativeQuery(
"select ID, NAME from SALES_CUSTOMER where NAME like ?1");
query.setParameter(1, "%Company%");
List list = query.getResultList();
for (Iterator it = list.iterator(); it.hasNext(); ) {
Object[] row = (Object[]) it.next();
UUID id = (UUID) row[0];
String name = (String) row[1];
}
如果选择了单个列或聚合函数,结果列表将直接包含这些值:
Query query = persistence.getEntityManager().createNativeQuery(
"select count(*) from SEC_USER where login = #login");
query.setParameter("login", "admin");
long count = (long) query.getSingleResult();
如果要生成的实体类型与查询语句一起传递给 EntityManager.createNativeQuery()
,则返回 TypedQuery
对象,并且 ORM 尝试将查询结果映射到相应的实体属性。例如:
TypedQuery<Customer> query = em.createNativeQuery(
"select * from SALES_CUSTOMER where NAME like ?1",
Customer.class);
query.setParameter(1, "%Company%");
List<Customer> list = query.getResultList();
在使用 SQL 时需要注意,对应于 UUID
类型的实体属性的列将以 UUID
类型或 String
类型返回,具体取决于所使用的 DBMS:
-
HSQLDB –
String
-
PostgreSQL –
UUID
-
Microsoft SQL Server –
String
-
Oracle –
String
-
MySQL –
String
此类型的参数也应该以 UUID
或字符串格式传递,具体取决于 DBMS。要确保代码不依赖于 DBMS 细节,请使用 DbTypeConverter
,它提供了在 Java 对象与 JDBC 参数和结果之间转换数据的方法。
原生查询语句支持位置和命名参数。位置参数在查询语句中以 ? 标记,后而跟从 1 开始的参数序号。命名参数用数字符号(#)标记。请参阅上面的示例。
与当前持久化上下文相关的返回实体的 SQL 查询和修改查询(update
、 delete
)的行为类似于上面描述的JPQL 查询。
3.4.4.6. 实体监听器
监听器是一个实现 com.haulmont.cuba.core.listener
包中的一个或多个接口的类。监听器将根据所实现的接口对相应的事件做出响应。
- BeforeDetachEntityListener
-
onBeforeDetach()
方法在事务提交时对象从 EntityManager 分离之前调用。此监听器可用于将非持久化实体属性发送到客户端层之前填充它们。
- BeforeAttachEntityListener
-
onBeforeAttach()
方法在执行了EntityManager.merge()
操作后,对象附加到持久化上下文之前调用。例如,可以使用此监听器在将持久化实体属性保存到数据库之前填充它们。
- BeforeInsertEntityListener
-
onBeforeInsert()
方法在将记录插入数据库之前调用。可以使用此方法中当前可用的EntityManager
执行所有类型的操作。 - AfterInsertEntityListener
-
onAfterInsert()
方法在将记录插入数据库之后但在事务提交之前调用。此方法不允许修改当前持久化上下文,但是,可以使用 QueryRunner 实现对数据库的更改。 - BeforeUpdateEntityListener
-
onBeforeUpdate()
方法在记录更新到数据库中之前调用。可以使用此方法中当前可用的EntityManager
执行所有类型的操作。 - AfterUpdateEntityListener
-
onAfterUpdate()
方法在将记录插入数据库之后但在事务提交之前调用。此方法不允许修改当前持久化上下文,但是,可以使用 QueryRunner 实现对数据库的更改。 - BeforeDeleteEntityListener
-
onBeforeDelete()
方法在从数据库中删除记录之前调用(在软删除的情况下是在更新记录之前)。可以使用此方法中当前可用的EntityManager
执行所有类型的操作。 - AfterDeleteEntityListener
-
onAfterDelete()
方法从数据库中删除记录后(在软删除的情况下是在更新记录之后),但在事务提交之前调用。此方法不允许修改当前持久化上下文,但是,可以使用QueryRunner
实现对数据库的更改。
实体监听器必须是托管 Bean,因此可以在字段和 setters 方法上使用注入。对于特定类的所有实例,一种类型的监听器只会创建一个实例,因此监听器应该是无状态的。
需要知道,对于 BeforeInsertEntityListener
,框架只会保证传入监听器的根实体为托管状态。该实体内对象关系图中其它对象的引用可能是 游离(detached) 状态。所以如果需要更新这些对象,需要用 EntityManager.merge()
方法,或者使用 EntityManager.find()
来访问其所有属性。示例:
package com.company.sample.listener;
import com.company.sample.core.DiscountCalculator;
import com.company.sample.entity.*;
import com.haulmont.cuba.core.EntityManager;
import com.haulmont.cuba.core.listener.*;
import org.springframework.stereotype.Component;
import javax.inject.Inject;
import java.math.BigDecimal;
@Component("sample_OrderEntityListener")
public class OrderEntityListener implements
BeforeInsertEntityListener<Order>,
BeforeUpdateEntityListener<Order>,
BeforeDeleteEntityListener<Order> {
@Inject
private DiscountCalculator discountCalculator; // a managed bean of the middle tier
@Override
public void onBeforeInsert(Order entity, EntityManager entityManager) {
calculateDiscount(entity.getCustomer(), entityManager);
}
@Override
public void onBeforeUpdate(Order entity, EntityManager entityManager) {
calculateDiscount(entity.getCustomer(), entityManager);
}
@Override
public void onBeforeDelete(Order entity, EntityManager entityManager) {
calculateDiscount(entity.getCustomer(), entityManager);
}
private void calculateDiscount(Customer customer, EntityManager entityManager) {
if (customer == null)
return;
// Delegate calculation to a managed bean of the middle tier
BigDecimal discount = discountCalculator.calculateDiscount(customer.getId());
// Merge customer instance because it comes to onBeforeInsert as part of another
// entity's object graph and can be detached
Customer managedCustomer = entityManager.merge(customer);
// Set the discount for the customer. It will be saved on transaction commit.
managedCustomer.setDiscount(discount);
}
}
除了 BeforeAttachEntityListener
之外,所有的监听器都在一个数据库事务中工作。也就是说,如果在监听器中抛出异常,当前事务会被回退,所有数据库的改动也会被丢弃。
如果需要在事务提交成功之后做一些操作,可以使用 Spring 的 TransactionSynchronization
回调函数在事务完成之后执行任务。示例:
package com.company.sales.service;
import com.company.sales.entity.Customer;
import com.haulmont.cuba.core.EntityManager;
import com.haulmont.cuba.core.listener.BeforeInsertEntityListener;
import com.haulmont.cuba.core.listener.BeforeUpdateEntityListener;
import org.springframework.stereotype.Component;
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
import org.springframework.transaction.support.TransactionSynchronizationManager;
@Component("sales_CustomerEntityListener")
public class CustomerEntityListener implements BeforeInsertEntityListener<Customer>, BeforeUpdateEntityListener<Customer> {
@Override
public void onBeforeInsert(Customer entity, EntityManager entityManager) {
printCustomer(entity);
}
@Override
public void onBeforeUpdate(Customer entity, EntityManager entityManager) {
printCustomer(entity);
}
private void printCustomer(Customer customer) {
System.out.println("In transaction: " + customer);
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
System.out.println("After transaction commit: " + customer);
}
});
}
}
- 注册实体监听器
-
可以通过两种方式为实体指定实体监听器:
-
静态方式 – 监听器的 bean 名称列在实体类的 @Listeners 注解中。
@Entity(...) @Table(...) @Listeners("sample_MyEntityListener") public class MyEntity extends StandardEntity { ... }
-
动态方式 – 将监听器的 bean 名称传递给
EntityListenerManager
bean 的addListener()
方法。这种方法可以为应用程序组件中的实体添加监听器。在下面的例子中,为框架定义的User
实体添加了一个监听器,监听器由sample_UserEntityListener
bean 实现:package com.company.sample.core; import com.haulmont.cuba.core.global.Events; import com.haulmont.cuba.core.sys.events.AppContextInitializedEvent; import com.haulmont.cuba.core.sys.listener.EntityListenerManager; import com.haulmont.cuba.security.entity.User; import org.springframework.context.event.EventListener; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import javax.inject.Inject; @Component("sample_AppLifecycle") public class AppLifecycle { @Inject private EntityListenerManager entityListenerManager; @EventListener(AppContextInitializedEvent.class) // notify after AppContext is initialized @Order(Events.LOWEST_PLATFORM_PRECEDENCE + 100) // run after all framework listeners public void initEntityListeners() { entityListenerManager.addListener(User.class, "sample_UserEntityListener"); } }
如果为一个实体声明了几个相同类型的监听器,有来自实体类及其父类的注解,还有动态添加的,则将按以下顺序调用它们:
-
对于每个被继承对象,从最远的父级对象开始,首先调用动态添加的监听器,然后是静态分配的监听器。
-
父类的调用完之后,首先调用给实体类动态添加的监听器,然后调用静态分配的。
-
3.4.5. 事务管理
本节介绍在 CUBA 应用程序中事务管理的各个方面。
3.4.5.1. 编程式事务管理
编程式事务管理使用 com.haulmont.cuba.core.Transaction
接口完成。可以通过Persistence基础接口的 createTransaction()
或 getTransaction()
方法获得对它的引用。
createTransaction()
方法创建一个新事务并返回 Transaction
接口。后续调用此接口的 commit()
、 commitRetaining()
、 end()
方法控制创建的事务。如果在创建时有另一个活动的事务,它将先暂停并在新创建的事务完成后恢复。
getTransaction()
方法要么创建新事务,要么附加到已有事务并返回一个嵌套事务。如果在调用时有一个活动的当前事务,那么该方法会成功完成,但后续调用嵌套事务的 commit()
、 commitRetaining()
、 end()
方法对当前事务没有影响。但是,在没有调用嵌套事务的 commit()
方法的情况下调用 end()
方法,会将当前事务标记为 RollbackOnly
。简单来说,就是只有嵌套事务成功提交了,外层事务才能提交。
编程式事务管理的示例:
@Inject
private Metadata metadata;
@Inject
private Persistence persistence;
...
// try-with-resources style
try (Transaction tx = persistence.createTransaction()) {
Customer customer = metadata.create(Customer.class);
customer.setName("John Smith");
persistence.getEntityManager().persist(customer);
tx.commit();
}
// plain style
Transaction tx = persistence.createTransaction();
try {
Customer customer = metadata.create(Customer.class);
customer.setName("John Smith");
persistence.getEntityManager().persist(customer);
tx.commit();
} finally {
tx.end();
}
Transaction
接口还有 execute()
方法接受 action 类或 lambda 表达式。action 类或 lambda 表达式表示的操作将在事务中执行。这样可以以函数式编程风格组织事务管理,例如:
UUID customerId = persistence.createTransaction().execute((EntityManager em) -> {
Customer customer = metadata.create(Customer.class);
customer.setName("ABC");
em.persist(customer);
return customer.getId();
});
Customer customer = persistence.createTransaction().execute(em ->
em.find(Customer.class, customerId, "_local"));
需要注意,给定 Transaction
实例的 execute()
方法只能调用一次,因为事务在执行完操作代码后结束。
3.4.5.2. 声明式事务管理
中间件托管 Bean的任何方法都可以使用 @org.springframework.transaction.annotation.Transactional
进行注解,这将在调用方法时自动创建事务。因此这种方法不需要调用 Persistence.createTransaction()
,可以直接获取 EntityManager
进行使用。
@Transactional
注解支持多个参数,包括:
-
propagation
- 事务创建模式。REQUIRED
值对应于getTransaction()
,REQUIRES_NEW
值对应于createTransaction()
。默认值为REQUIRED
。@Transactional(propagation = Propagation.REQUIRES_NEW) public void doSomething() { }
-
value
- 数据存储名称。如果省略,则使用主数据存储。例如:@Transactional("db1") public void doSomething() { }
声明式事务管理可以减少 脚手架代码 的数量,但它有以下缺点:事务在应用程序代码之外提交,这通常会使调试变得复杂,因为它不会暴露将更改发送到数据库和实体变为Detached的时刻。另外,需要注意,声明性标记仅在容器调用方法时才有效,即从同一对象的另一个方法调用事务方法不会启动事务。
考虑到这一点,我们建议仅对简单的情况使用声明式事务管理,例如服务方法方法读取某个对象并将其返回给客户端。
3.4.5.3. 事务交互示例
- 嵌套事务的回滚
-
如果嵌套事务是通过
getTransaction()
创建并回滚,则无法提交外层事务。例如:void methodA() { Transaction tx = persistence.createTransaction(); try { methodB(); (1) tx.commit(); (4) } finally { tx.end(); } } void methodB() { Transaction tx = persistence.getTransaction(); try { tx.commit(); (2) } catch (Exception e) { return; (3) } finally { tx.end(); } }
1 调用方法创建嵌套事务 2 假设发生异常 3 处理异常并退出 4 在这里将抛出异常,因为事务被标记为仅回滚(rollback only)。 如果使用
createTransaction()
创建methodB()
中的事务,那么回滚它将不会影响methodA()
中的外层事务。 - 在嵌套事务中读取和修改数据
-
首先看一下使用
getTransaction()
创建的依赖嵌套事务:void methodA() { Transaction tx = persistence.createTransaction(); try { EntityManager em = persistence.getEntityManager(); Employee employee = em.find(Employee.class, id); (1) assertEquals("old name", employee.getName()); employee.setName("name A"); (2) methodB(); (3) tx.commit(); (8) } finally { tx.end(); } } void methodB() { Transaction tx = persistence.getTransaction(); try { EntityManager em = persistence.getEntityManager(); (4) Employee employee = em.find(Employee.class, id); (5) assertEquals("name A", employee.getName()); (6) employee.setName("name B"); tx.commit(); (7) } finally { tx.end(); } }
1 使用 name == "old name" 加载实体 2 给字段设置新值 3 调用方法创建嵌套事务 4 获取与方法 methodA 中相同的 EntityManager 实例 5 使用同样的标识符加载实体 6 字段值是新的,因为我们使用相同的持久化上下文,并且根本没有调用 DB 7 此时不进行实际的提交 8 更改提交到 DB,它将包含 "name B" 现在,看一下使用
createTransaction()
创建的独立嵌套事务的相同示例:void methodA() { Transaction tx = persistence.createTransaction(); try { EntityManager em = persistence.getEntityManager(); Employee employee = em.find(Employee.class, id); (1) assertEquals("old name", employee.getName()); employee.setName("name A"); (2) methodB(); (3) tx.commit(); (8) } finally { tx.end(); } } void methodB() { Transaction tx = persistence.createTransaction(); try { EntityManager em = persistence.getEntityManager(); (4) Employee employee = em.find(Employee.class, id); (5) assertEquals("old name", employee.getName()); (6) employee.setName("name B"); (7) tx.commit(); } finally { tx.end(); } }
1 使用 name == "old name" 加载实体 2 给字段设置新值 3 调用方法创建嵌套事务 4 创建新的 EntityManager 实例, 因为这是一个新事务 5 使用相同的标识符加载一个实体 6 字段值是旧的,因为一个旧的实体实例被从数据库加载了 7 变更被提交到 DB,现在 "name B"值被存储到数据 DB 8 由于启用乐观锁,这里将发生异常,提交失败 在最后一个例子中,只有当实体支持乐观锁时,即只有它实现了
Versioned
接口时,才会发生第(8)点的异常。
3.4.5.4. 事务参数
- 事务超时
-
可以为创建的事务设置超时时限(以秒为单位)。如果发生超时,事务将被中断并回滚。事务超时设置可以有效地限制数据库请求的最大持续时间。
以编程方式管理事务时,通过传递给
Persistence.createTransaction()
方法的参数TransactionParams
对象来指定超时。例如:Transaction tx = persistence.createTransaction(new TransactionParams().setTimeout(2));
在声明式事务管理的情况下,使用
@Transactional
注解的timeout
参数:@Transactional(timeout = 2) public void someServiceMethod() { ...
可以使用cuba.defaultQueryTimeoutSec应用程序属性定义默认超时时限。
- 只读事务
-
如果事务仅用于从数据库读取数据,则可以将事务标记为只读。例如,DataManager的所有
load
方法默认使用只读事务。只读事务会有更好的性能,因为平台不执行处理可能修改实体的代码。BeforeCommit
事务监听器也不会被调用。如果只读事务的持久化上下文包含已修改的实体,则在提交事务时将抛出
IllegalStateException
。这意味着只有在确定不修改任何实体时,才应将事务标记为只读。以编程方式管理事务时,通过将
TransactionParams
对象传递给Persistence.createTransaction()
方法来指定只读标识。例如:Transaction tx = persistence.createTransaction(new TransactionParams().setReadOnly(true));
在声明式事务管理的情况下,使用
@Transactional
注解的readOnly
参数:@Transactional(readOnly = true) public void someServiceMethod() { ...
3.4.5.5. 事务监听器
事务监听器旨在对事务生命周期事件做出响应。与实体监听器不同,它们不与何实体类型绑定,可以被每个事务调用。
监听器是一个托管 Bean,它实现了 BeforeCommitTransactionListener
或 AfterCompleteTransactionListener
接口或者同时实现这两个接口。
- BeforeCommitTransactionListener
-
如果事务不是只读的,则在所有实体监听器之后,事务提交之前调用
beforeCommit()
方法。该方法接受当前持久化上下文中的实体集合和当前的EntityManager作为参数。监听器可用于执行涉及多个实体的复杂业务规则。在下面的例子中,
Order
实体的amount
属性必须根据订单中的discount
值计算,OrderLine
实体的price
和quantity
构成订单。@Component("demo_OrdersTransactionListener") public class OrdersTransactionListener implements BeforeCommitTransactionListener { @Inject private PersistenceTools persistenceTools; @Override public void beforeCommit(EntityManager entityManager, Collection<Entity> managedEntities) { // gather all orders affected by changes in the current transaction Set<Order> affectedOrders = new HashSet<>(); for (Entity entity : managedEntities) { // skip not modified entities if (!persistenceTools.isDirty(entity)) continue; if (entity instanceof Order) affectedOrders.add((Order) entity); else if (entity instanceof OrderLine) { Order order = ((OrderLine) entity).getOrder(); // a reference can be detached, so merge it into current persistence context affectedOrders.add(entityManager.merge(order)); } } // calculate amount for each affected order by its lines and discount for (Order order : affectedOrders) { BigDecimal amount = BigDecimal.ZERO; for (OrderLine orderLine : order.getOrderLines()) { if (!orderLine.isDeleted()) { amount = amount.add(orderLine.getPrice().multiply(orderLine.getQuantity())); } } BigDecimal discount = order.getDiscount().divide(BigDecimal.valueOf(100), 2, BigDecimal.ROUND_DOWN); order.setAmount(amount.subtract(amount.multiply(discount))); } } }
- AfterCompleteTransactionListener
-
事务完成后调用
afterComplete()
方法。该方法接受一个参数,该参数表明事务是否已成功提交,以及已完成事务的持久化上下文中包含的已分离实体的集合。用法示例:
@Component("demo_OrdersTransactionListener") public class OrdersTransactionListener implements AfterCompleteTransactionListener { private Logger log = LoggerFactory.getLogger(OrdersTransactionListener.class); @Override public void afterComplete(boolean committed, Collection<Entity> detachedEntities) { if (!committed) return; for (Entity entity : detachedEntities) { if (entity instanceof Order) { log.info("Order: " + entity); } } } }
3.4.6. 实体以及查询语句缓存
- 实体缓存
-
实体缓存由 EclipseLink ORM 框架提供。它将内存中最近读取或写入的实体实例存储,从而最大限度地减少数据库访问并提高应用程序性能
实体缓存仅在根据 ID 检索实体时使用,因此根据其它属性的查询仍在数据库上执行。但是,如果相关实体位于缓存中,则这些查询可以更简单、更快速。例如,如果查询与客户相关的订单并且不使用缓存,则 SQL 查询将包含客户表的 JOIN 关联。如果客户实体被缓存,则 SQL 查询将仅选择订单,并且将从缓存中检索相关客户。
要启用实体缓存,请在 core 模块的 app.properties 文件中设置以下属性:
-
eclipselink.cache.shared.sales$Customer = true
- 启用sales$Customer
实体的缓存。 -
eclipselink.cache.size.sales$Customer = 500
- 将sales$Customer
的缓存大小设置为 500 个实例。默认大小为 100。如果启用了实体缓存,则始终建议增加缓存大小的值。否则,如果查询返回的记录数超过 100,则将对查询结果的每条记录执行大量的获取操作。
实体是否被缓存会影响平台选择的用于加载实体关系图的获取模式。如果引用属性是可缓存的实体,则获取模式始终为
UNDEFINED
,这允许 ORM 从缓存中检索引用,而不是使用 JOIN 执行查询或单独的批量查询。平台在中间件集群中提供实体缓存协调机制。在一个群集节点上更新或删除缓存实体时,其它节点(如果有)上的相同缓存实例将失效,因此使用此实例的下一个操作将从数据库中读取新状态。
-
- 查询缓存
-
查询缓存存储由 JPQL 查询返回的实体实例的标识符,因此它很自然地补充了实体缓存机制。
例如,如果为实体启用了实体缓存(例如,
sales$Customer
),并且首次执行查询语句select c from sales$Customer c where c.grade = :grade
,则会发生以下情况:-
ORM 在数据库上运行查询。
-
已加载的
Customer
实例放置在实体缓存中。 -
查询文本和返回实例的标识符列表参数的映射被放到查询缓存中。
当第二次使用相同的参数执行相同的查询时,平台会在查询缓存中查找查询结果,并通过标识符从实体缓存中加载实体实例。不需要数据库操作。
默认情况下不缓存查询。可以指定应用程序的不同层缓存查询:
-
使用 EntityManager 时,使用 Query 接口的
setCacheable()
方法。 -
使用DataManager时,使用
LoadContext.Query
接口的setCacheable()
方法。 -
使用datasources时,使用
CollectionDatasource
接口的setCacheable()
方法或cacheable
XML 属性
仅在为返回的实体启用实体缓存时才使用可缓存查询。否则,每个查询实体实例将通过其标识符逐个从数据库中获取。
app-core.cuba:type=QueryCacheSupport
JMX-bean 可用于监视缓存状态并手动释放缓存的查询。例如,如果已直接在数据库中修改了sales$Customer
实体的实例,则应使用带有sales$Customer
参数的evict()
操作释放该实体的所有缓存的查询。以下应用程序属性会影响查询缓存:
-
3.4.7. EntityChangedEvent
EntityChangedEvent
是一个 Spring 的 ApplicationEvent
事件,会在实体保存到数据库时从中间层发送该事件。可以在事务中或者事务完成后处理该事件(使用 @TransactionalEventListener )。只有实体使用了 @PublishEntityChangedEvents
注解,才会发送该事件。
EntityChangedEvent
不包含更改的实体本身,而只包含了其 id。还有,getOldValue(attributeName)
方法也只会返回引用的 id 而不是对象。所以如果需要的话,开发者可以使用合适的视图或者其它参数重新加载实体。
下面的例子展示了在当前事务中和事务后处理 Customer
实体的 EntityChangedEvent
事件。
package com.company.demo.core;
import com.company.demo.entity.Customer;
import com.haulmont.cuba.core.app.events.AttributeChanges;
import com.haulmont.cuba.core.app.events.EntityChangedEvent;
import com.haulmont.cuba.core.entity.contracts.Id;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
import java.util.UUID;
@Component("demo_CustomerChangedListener")
public class CustomerChangedListener {
@EventListener (1)
private void beforeCommit(EntityChangedEvent<Customer, UUID> event) {
Id<Customer, UUID> entityId = event.getEntityId(); (2)
EntityChangedEvent.Type changeType = event.getType(); (3)
AttributeChanges changes = event.getChanges();
if (changes.isChanged("name")) { (4)
String oldName = changes.getOldValue("name"); (5)
// ...
}
}
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) (6)
private void afterCommit(EntityChangedEvent<Customer, UUID> event) {
(7)
}
}
1 | - 该监听器会在当前事务中调用。使用 @EventListener 注解跟 @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT) 是一样的。 |
2 | - 更改实体的 id。 |
3 | - 更改类型: CREATED 、UPDATED 或 DELETED 。 |
4 | - 可以检查是否某个特定属性有变化。 |
5 | - 可以获取变化属性的旧值。 |
6 | - 该监听器在事务提交之后会被调用。 |
7 | - 在事务提交之后,事件包含跟提交之前相同的信息。 |
如果监听器在事务内部调用,可以通过抛出异常的方法回滚事务,这样不会有数据保存至数据库。如果不想用户看到任何错误提示,可以用 SilentException
。
如果在当前事务中处理 EntityChangedEvent
,可以使用 TransactionalDataManager 从数据库获取更改实体的当前状态。如果在事务提交之后的监听器中,可以使用 DataManager
来创建新事务并加载数据。
别忘了为需要监听 |
3.4.8. 系统身份验证
执行用户请求时,中间件程序代码始终可以通过UserSessionSource接口访问当前用户的信息。这是可能的,因为当从客户端层收到请求时,会自动为当前线程设置相应的SecurityContext对象。
但是,在某些情况下当前线程与任何系统用户都没有关联,例如,从定时任务或通过 JMX 接口调用 bean 的方法时。如果 bean 修改数据库中的实体,则需要实施更改的用户的信息,即身份验证。
这种身份验证称为“系统身份验证”,因为它不需要用户参与 - 应用程序中间层只是创建或使用现有的用户会话,为当前线程设置相应的 SecurityContext
对象。
可以使用以下方法为代码块提供系统身份验证:
-
使用
com.haulmont.cuba.security.app.Authentication
bean:@Inject protected Authentication authentication; ... authentication.begin(); try { // authenticated code } finally { authentication.end(); }
-
在 bean 的方法上添加
@Authenticated
注解:@Authenticated public String foo(String value) { // authenticated code }
第二种情况通过 AuthenticationInterceptor
对象隐式使用 Authentication
bean,该对象拦截所有 @Authenticated
注解 bean 方法。
在上面的示例中,将代表一个用户创建用户会话,该用户的登录名在cuba.jmxUserLogin应用程序属性中指定。如果需要代表另一个用户进行身份验证,请将所需用户的登录名传递给第一种变体的 begin()
方法。
如果当前线程在执行 例如,如果 bean 与用户当前连接的 Web 客户端 block 位于同一 JVM 中,则执行内置JMX console的 Web 客户端对 JMX bean 方法的调用将使用当前登录的用户的信息,而忽略系统身份验证。 |
3.5. 通用用户界面(GUI)
通用用户界面 (Generic UI, GUI) 框架可以使用 Java 和 XML 来创建 UI 界面。XML 是方式是可选的,但是使用这个方式可以声明式的创建界面布局并且减少构建用户界面的代码量。
应用程序的界面包含了以下部分:
应用程序界面的代码跟可视化组件接口(VCL 接口)交互。这些接口通过使用 Vaadin 框架组件实现。
可视化组件库(VCL)包含大量即用型组件。
数据组件为可视化组件绑定到实体以及在界面控制器中处理实体提供统一的接口。
客户端的基础设施包含包含主应用程序窗口和其他的通用客户端机制。
3.5.1. 界面
界面(Screen)是通用 UI 的主要部分。它由可视化组件、数据容器和其它非可视化组件组成。界面可以显示在应用程序主窗口的标签页中,也可以显示为模式对话框。
要显示一个界面,框架会创建一个可视化组件 Window
的新实例,将窗口与界面控制器连接起来,并将界面布局组件作为窗口的子组件加载。最终,界面的窗口将被添加到应用程序主窗口中。
3.5.1.1. 界面控制器
界面控制器是一个 Java 或 Groovy 类,包含界面初始化和事件处理逻辑。通常,控制器链接到XML 描述,XML 描述中定义了界面布局和数据容器,但也可以以编程方式创建所有可视化组件和非可视化组件。
所有界面控制器都实现了 FrameOwner
标记接口。此接口的名称表示它引用了一个框架(frame),框架是一个在主应用程序窗口中显示界面的可视化组件。框架有两种类型:
-
Window
- 一个独立的窗口,可以显示在应用程序主窗口内的标签页中,也可以显示为模式对话框。 -
Fragment
- 一个轻量级组件,可以被添加到窗口或其它 Fragment。
根据所使用的框架,控制器被分为两个不同的类别:
-
Screen
- 窗口控制器的基类。 -
ScreenFragment
- fragment 控制器的基类。
Screen
类为所有独立界面提供大部分基本的功能。还有其它一些特定的界面基类可用于处理实体:
-
StandardEditor
- 实体编辑界面的基类。 -
StandardLookup
- 实体浏览和查找界面的基类。 -
MasterDetailScreen
- 组合界面,在左侧显示实体列表、在右侧显示所选实体的详细信息。
3.5.1.1.1. 界面控制器注解
控制器上的类级别的注解用于向框架提供界面相关的信息。一部分注解适用于任何类型的界面,也有一部分仅用于实体编辑或查找界面。
以下示例演示了常见的界面注解的用法:
package com.company.demo.web.screens;
import com.haulmont.cuba.gui.screen.*;
@UiController("demo_FooScreen")
@UiDescriptor("foo-screen.xml")
@LoadDataBeforeShow
@MultipleOpen
@DialogMode(forceDialog = true)
public class FooScreen extends Screen {
}
-
@UiController
注解表示该类是一个界面控制器。注解的值是界面的 ID,可用于从主菜单或以编程方式打开界面时引用界面。
-
@UiDescriptor
注解将界面控制器连接到界面 XML 描述。注解的值指定描述文件的路径。如果该值仅包含文件名,则假定该文件与控制器类位于同一个包中。
-
@LoadDataBeforeShow
注解表示在显示界面之前应自动触发所有数据加载器。准确的说,数据会在调用所有的 BeforeShowEvent 监听器之后,但是在 AfterShowEvent 监听器之前加载。如果需要在界面显示之前的加载数据时刻执行某些操作,删除此注解或将其值设置为false
,这样可以在BeforeShowEvent
事件监听器中使用getScreenData().loadAll()
方法或个别加载器的load()
方法来手动加载数据。
-
@MultipleOpen
注解表示可以从主菜单多次打开界面。默认情况下,当用户点击主菜单项时,框架会检查是否已在主窗口标签页上打开相同类和 ID 的界面。如果找到此类界面,则会关闭该界面,并在新标签页中打开新的界面实例。当存在@MultipleOpen
注解时,不执行任何检查,只在新标签页中打开一个新的界面实例。可以通过覆盖界面控制器中的
isSameScreen()
方法,提供自定义的界面实例同一性检查方法。
-
@DialogMode
注解允许指定界面以对话框窗口方式打开时的尺寸和行为。它对应于界面描述的<dialogMode>
元素,可以替代使用。对于除forceDialog
之外的所有参数,XML 中的设置优先于注解。forceDialog
参数会合并生效,即:只要在注解或 XML 其中之一将其设置为 true,界面就总是以对话框的方式打开。
针对查找界面的注解示例:
package com.company.demo.web.screens;
import com.haulmont.cuba.gui.screen.*;
import com.company.demo.entity.Customer;
// common annotations
@UiController("demo_Customer.browse")
@UiDescriptor("customer-browse.xml")
@LoadDataBeforeShow
// lookup-specific annotations
@LookupComponent("customersTable")
@PrimaryLookupScreen(Customer.class)
public class CustomerBrowse extends StandardLookup<Customer> {
}
-
@LookupComponent
注解指定一个 UI 组件的 ID,这个组件用于获取查找界面的返回值。可以通过覆盖界面控制器的
getLookupComponent()
方法以编程方式指定查找界面,而不是使用注解。
-
@PrimaryLookupScreen
注解表示此界面是指定实体类型的默认查找界面。注解比{entity_name}.lookup / {entity_name}.browse
名称约定具有更高的优先级。
编辑器界面特有的注解示例:
package com.company.demo.web.data.sort;
import com.haulmont.cuba.gui.screen.*;
import com.company.demo.entity.Customer;
// common annotations
@UiController("demo_Customer.edit")
@UiDescriptor("customer-edit.xml")
@LoadDataBeforeShow
// editor-specific annotations
@EditedEntityContainer("customerDc")
@PrimaryEditorScreen(Customer.class)
public class CustomerEdit extends StandardEditor<Customer> {
}
-
@EditedEntityContainer
注解指定一个数据容器,这个数据容器包含被编辑的实体。不使用注解的话,可以通过覆盖界面控制器的
getEditedEntityContainer()
方法以编程方式指定容器。
-
@PrimaryEditorScreen
注解表示此界面是指定实体类型的默认编辑界面。 注解比{entity_name}.edit
名称约定具有更高的优先级。
3.5.1.1.2. 界面控制器事件
本节介绍可以在界面控制器中处理的生命周期事件。
-
InitEvent
在界面控制器及其所有以声明方式定义的组件创建后并完成依赖注入时发送的事件。此时,嵌套的界面 fragment 尚未初始化,某些可视化组件未完全初始化,例如按钮还未与操作关联起来。@Subscribe protected void onInit(InitEvent event) { Label<String> label = uiComponents.create(Label.TYPE_STRING); label.setValue("Hello World"); getWindow().add(label); }
-
AfterInitEvent
在界面控制器及其所有以声明方式定义的组件被创建并完成依赖注入,并且所有组件都已完成其内部初始化过程时发送此事件。此时,嵌套的界面 fragment(如果有的话)已经发送了InitEvent
和AfterInitEvent
事件。在此事件监听器中,可以创建可视化组件或数据组件并且进行一些依赖于嵌套 fragment 的额外初始化过程。
-
InitEntityEvent
继承自StandardEditor
和MasterDetailScreen
的界面中,在新实体实例设置给被编辑实体的容器之前发送的事件。使用此事件监听器初始化新实体实例中的默认值,例如:@Subscribe protected void onInitEntity(InitEntityEvent<Foo> event) { event.getEntity().setStatus(Status.ACTIVE); }
-
BeforeShowEvent
在界面将要展示之前发送的事件,此时,界面尚未被添加到应用程序 UI 中、UI 组件已经应用安全限制、保存的组件设置尚未应用到 UI 组件。对于使用@LoadDataBeforeShow
注解的界面,尚未加载数据。在此事件监听器中,可以加载数据、检查权限和修改 UI 组件。例如:@Subscribe protected void onBeforeShow(BeforeShowEvent event) { customersDl.load(); }
-
AfterShowEvent
在显示界面之后立即发送此事件,此时,界面已经被添加到应用程序 UI 中。保存的组件设置已应用到 UI 组件。在此事件监听器中,可以显示通知、对话框或其它界面。例如:@Subscribe protected void onAfterShow(AfterShowEvent event) { notifications.create().withCaption("Just opened").show(); }
-
BeforeCloseEvent
在界面通过其close(CloseAction)
方法关闭之前发送的事件。此刻界面仍然正常显示并且功能完整。组件设置尚未保存。在此事件监听器中,可以做任何条件检查并使用事件的preventWindowClose()
方法阻止界面关闭,例如:@Subscribe protected void onBeforeClose(BeforeCloseEvent event) { if (Strings.isNullOrEmpty(textField.getValue())) { notifications.create().withCaption("Input required").show(); event.preventWindowClose(); } }
还有一个同名的事件,它定义在
Window
接口内。此事件会在界面被外部(界面控制器之外)操作关闭之前发送,比如点击窗口标签页的关闭按钮或者按下 Esc 键。可以使用getCloseOrigin()
方法获取窗口关闭的类型,此方法返回一个实现了CloseOrigin
接口的对象。CloseOrigin
接口的默认实现是`CloseOriginType`,有三个值:-
BREADCRUMBS
- 界面通过点击面包屑链接关闭。 -
CLOSE_BUTTON
- 界面通过窗口顶部的关闭按钮关闭,或者通过窗口标签页的关闭按钮或右键菜单的操作:关闭、关闭全部、关闭其它。 -
SHORTCUT
- 界面通过 cuba.gui.closeShortcut 应用程序属性定义的快捷键关闭。可以通过在
@Subscribe
注解中指定Target.FRAME
的方法来订阅Window.BeforeCloseEvent
:@Subscribe(target = Target.FRAME) protected void onBeforeClose(Window.BeforeCloseEvent event) { if (event.getCloseOrigin() == CloseOriginType.BREADCRUMBS) { event.preventWindowClose(); } }
-
-
AfterCloseEvent
在界面通过close(CloseAction)
方法关闭并且在Screen.AfterDetachEvent
之后发送的事件。组件设置已保存。在此事件监听器中,可以在关闭界面后显示通知或对话框,例如:@Subscribe protected void onAfterClose(AfterCloseEvent event) { notifications.create().withCaption("Just closed").show(); }
-
AfterDetachEvent
在用户关闭界面或用户注销时从应用程序 UI 中移除界面后发送的事件。此事件监听器可用于释放界面持有的资源。请注意,在 HTTP 会话过期时不会发送此事件。
-
UrlParamsChangedEvent
打开的界面对应的浏览器 URL 参数更改时发送的事件。事件在显示界面之前触发,以便能够进行一些准备工作。在此事件监听器中,可以根据新的参数加载一些数据或更改界面控件的状态:@Subscribe protected void onUrlParamsChanged(UrlParamsChangedEvent event) { Map<String, String> params = event.getParams(); // handle new params }
3.5.1.1.3. 界面控制器注解
控制器上的类级别的注解用于向框架提供界面相关的信息。一部分注解适用于任何类型的界面,也有一部分仅用于实体编辑或查找界面。
以下示例演示了常见的界面注解的用法:
package com.company.demo.web.screens;
import com.haulmont.cuba.gui.screen.*;
@UiController("demo_FooScreen")
@UiDescriptor("foo-screen.xml")
@LoadDataBeforeShow
@MultipleOpen
@DialogMode(forceDialog = true)
public class FooScreen extends Screen {
}
-
@UiController
注解表示该类是一个界面控制器。注解的值是界面的 ID,可用于从主菜单或以编程方式打开界面时引用界面。
-
@UiDescriptor
注解将界面控制器连接到界面 XML 描述。注解的值指定描述文件的路径。如果该值仅包含文件名,则假定该文件与控制器类位于同一个包中。
-
@LoadDataBeforeShow
注解表示在显示界面之前应自动触发所有数据加载器。准确的说,数据会在调用所有的 BeforeShowEvent 监听器之后,但是在 AfterShowEvent 监听器之前加载。如果需要在界面显示之前的加载数据时刻执行某些操作,删除此注解或将其值设置为false
,这样可以在BeforeShowEvent
事件监听器中使用getScreenData().loadAll()
方法或个别加载器的load()
方法来手动加载数据。
-
@MultipleOpen
注解表示可以从主菜单多次打开界面。默认情况下,当用户点击主菜单项时,框架会检查是否已在主窗口标签页上打开相同类和 ID 的界面。如果找到此类界面,则会关闭该界面,并在新标签页中打开新的界面实例。当存在@MultipleOpen
注解时,不执行任何检查,只在新标签页中打开一个新的界面实例。可以通过覆盖界面控制器中的
isSameScreen()
方法,提供自定义的界面实例同一性检查方法。
-
@DialogMode
注解允许指定界面以对话框窗口方式打开时的尺寸和行为。它对应于界面描述的<dialogMode>
元素,可以替代使用。对于除forceDialog
之外的所有参数,XML 中的设置优先于注解。forceDialog
参数会合并生效,即:只要在注解或 XML 其中之一将其设置为 true,界面就总是以对话框的方式打开。
针对查找界面的注解示例:
package com.company.demo.web.screens;
import com.haulmont.cuba.gui.screen.*;
import com.company.demo.entity.Customer;
// common annotations
@UiController("demo_Customer.browse")
@UiDescriptor("customer-browse.xml")
@LoadDataBeforeShow
// lookup-specific annotations
@LookupComponent("customersTable")
@PrimaryLookupScreen(Customer.class)
public class CustomerBrowse extends StandardLookup<Customer> {
}
-
@LookupComponent
注解指定一个 UI 组件的 ID,这个组件用于获取查找界面的返回值。可以通过覆盖界面控制器的
getLookupComponent()
方法以编程方式指定查找界面,而不是使用注解。
-
@PrimaryLookupScreen
注解表示此界面是指定实体类型的默认查找界面。注解比{entity_name}.lookup / {entity_name}.browse
名称约定具有更高的优先级。
编辑器界面特有的注解示例:
package com.company.demo.web.data.sort;
import com.haulmont.cuba.gui.screen.*;
import com.company.demo.entity.Customer;
// common annotations
@UiController("demo_Customer.edit")
@UiDescriptor("customer-edit.xml")
@LoadDataBeforeShow
// editor-specific annotations
@EditedEntityContainer("customerDc")
@PrimaryEditorScreen(Customer.class)
public class CustomerEdit extends StandardEditor<Customer> {
}
-
@EditedEntityContainer
注解指定一个数据容器,这个数据容器包含被编辑的实体。不使用注解的话,可以通过覆盖界面控制器的
getEditedEntityContainer()
方法以编程方式指定容器。
-
@PrimaryEditorScreen
注解表示此界面是指定实体类型的默认编辑界面。 注解比{entity_name}.edit
名称约定具有更高的优先级。
3.5.1.2. 界面 XML 描述
界面描述具有 window
根元素。
根元素属性:
界面描述的元素:
-
data
− 定义界面的数据组件。 -
dialogMode
- 定义界面作为对话框打开时的尺寸及行为的设置。dialogMode
的属性:-
closeable
- 定义对话框窗口是否有关闭按钮。可选值:true
、false
。 -
closeOnClickOutside
- 当窗口是模式窗口时,定义是否允许通过单击窗口之外的区域来关闭对话框窗口。可选值:true
、false
。 -
forceDialog
- 指定界面应始终以对话框方式打开,无论在调用代码中选择了哪种WindowManager.OpenType
。可选值:true
、false
。 -
height
- 设置对话框窗口的高度。 -
maximized
- 如果设置了true
值,则对话窗口将最大化显示。可选值:true
、false
。 -
modal
-指定对话框窗口是否是模式窗口。可选值:true
、false
。 -
positionX
- 设置对话框窗口左上角的x
坐标。 -
positionY
- 设置对话框窗口左上角的y
坐标。 -
resizable
- 定义用户是否可以更改对话窗口的大小。可选值:true
、false
。 -
width
- 设置对话框窗口的宽度。
例如:
<dialogMode height="600" width="800" positionX="200" positionY="200" forceDialog="true" closeOnClickOutside="false" resizable="true"/>
-
-
actions
– 定义界面的操作列表。 -
timers
– 定义界面的计时器列表。 -
layout
− 界面布局的根元素。
3.5.1.3. 打开界面
- 使用 Screens 接口
-
Screens
接口允许创建和显示任何类型的界面。假设有一个用于显示具有一些特殊格式的消息的界面:
界面控制器@UiController("demo_FancyMessageScreen") @UiDescriptor("fancy-message-screen.xml") @DialogMode(forceDialog = true, width = "300px") public class FancyMessageScreen extends Screen { @Inject private Label<String> messageLabel; public void setFancyMessage(String message) { (1) messageLabel.setValue(message); } @Subscribe("closeBtn") protected void onCloseBtnClick(Button.ClickEvent event) { closeWithDefaultAction(); } }
1 - 一个界面参数 界面描述<?xml version="1.0" encoding="UTF-8" standalone="no"?> <window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd" caption="Fancy Message"> <layout> <label id="messageLabel" value="A message" stylename="h1"/> <button id="closeBtn" caption="Close"/> </layout> </window>
那么可以从另一个界面创建并打开它,如下所示:
@Inject private Screens screens; private void showFancyMessage(String message) { FancyMessageScreen screen = screens.create(FancyMessageScreen.class); screen.setFancyMessage(message); screens.show(screen); }
请注意这里是如何创建界面实例、为其提供参数,然后显示界面。
如果界面不需要来自调用方的任何参数,可以仅用一行代码创建并打开它:
@Inject private Screens screens; private void showDefaultFancyMessage() { screens.create(FancyMessageScreen.class).show(); }
screens
不是 Spring bean,所以只能将它注入到界面控制器或使用ComponentsHelper.getScreenContext(component).getScreens()
静态方法获取。
- 使用 ScreenBuilders bean
-
通过
ScreenBuilders
bean 可以使用各种参数打开所有类型的界面。下面是用它打开界面并且在界面关闭之后执行一些代码的例子(更多细节参考这里):@Inject private ScreenBuilders screenBuilders; @Inject private Notifications notifications; private void openOtherScreen() { screenBuilders.screen(this) .withScreenClass(OtherScreen.class) .withAfterCloseListener(e -> { notifications.create().withCaption("Closed").show(); }) .build() .show(); }
下面我们看看如何操作编辑界面和查找界面。
为
Customer
实体实例打开默认编辑界面的示例:@Inject private ScreenBuilders screenBuilders; private void editSelectedEntity(Customer entity) { screenBuilders.editor(Customer.class, this) .editEntity(entity) .build() .show(); }
在这种情况下,编辑界面将更新实体,但调用界面将不会接收到更新后的实例。
最常见的情况是需要编辑某些用
Table
或DataGrid
组件显示的实体。那么应该使用以下调用方式,它更简洁且能自动更新表格组件:@Inject private GroupTable<Customer> customersTable; @Inject private ScreenBuilders screenBuilders; private void editSelectedEntity() { screenBuilders.editor(customersTable).build().show(); }
要创建一个新的实体实例并打开它的编辑界面,只需在构建器上调用
newEntity()
方法:@Inject private GroupTable<Customer> customersTable; @Inject private ScreenBuilders screenBuilders; private void createNewEntity() { screenBuilders.editor(customersTable) .newEntity() .build() .show(); }
默认编辑界面的确定过程如下:
-
如果存在使用@PrimaryEditorScreen注解的编辑界面,则使用它。
-
否则,使用 id 是
{entity_name}.edit
的编辑界面(例如,sales_Customer.edit
)。
界面构建器提供了许多方法来设置被打开界面的可选参数。例如,以下代码以对话框的方式打开的特定编辑界面,同时新建并初始化实体:
@Inject private GroupTable<Customer> customersTable; @Inject private ScreenBuilders screenBuilders; private void editSelectedEntity() { screenBuilders.editor(customersTable).build().show(); } private void createNewEntity() { screenBuilders.editor(customersTable) .newEntity() .withInitializer(customer -> { // lambda to initialize new instance customer.setName("New customer"); }) .withScreenClass(CustomerEdit.class) // specific editor screen .withLaunchMode(OpenMode.DIALOG) // open as modal dialog .build() .show(); }
实体查找界面也能使用不同参数打开。
下面是打开
User
实体的默认查找界面的示例:@Inject private TextField<String> userField; @Inject private ScreenBuilders screenBuilders; private void lookupUser() { screenBuilders.lookup(User.class, this) .withSelectHandler(users -> { User user = users.iterator().next(); userField.setValue(user.getName()); }) .build() .show(); }
如果需要将找到的实体设置到字段,可使用更简洁的方式:
@Inject private PickerField<User> userPickerField; @Inject private ScreenBuilders screenBuilders; private void lookupUser() { screenBuilders.lookup(User.class, this) .withField(userPickerField) // set result to the field .build() .show(); }
默认查找界面的确定过程如下:
-
如果存在使用@PrimaryLookupScreen注解的查找界面,则使用它。
-
否则,如果存在 id 为
{entity_name}.lookup
的界面,则使用它(例如,sales_Customer.lookup
)。 -
否则,使用 id 为
{entity_name}.browse
的界面(例如,sales_Customer.browse
)。
与使用编辑界面一样,使用构建器方法设置打开界面的可选参数。例如,以下代码以对话框的方式打开特定的查找界面,在这个界面中查找
User
实体:@Inject private TextField<String> userField; @Inject private ScreenBuilders screenBuilders; private void lookupUser() { screenBuilders.lookup(User.class, this) .withScreenId("sec$User.browse") // specific lookup screen .withLaunchMode(OpenMode.DIALOG) // open as modal dialog .withSelectHandler(users -> { User user = users.iterator().next(); userField.setValue(user.getName()); }) .build() .show(); }
-
- 为界面传递参数
-
为打开界面传递参数的推荐方式是使用界面控制器的公共 setter 方法,如上面界面接口部分示范。
使用这个方式,可以为任意类型的界面传递参数,包括使用ScreenBuilders打开的实体编辑和查找界面。带有传参使用
ScreenBuilders
来调用FancyMessageScreen
如下所示:@Inject private ScreenBuilders screenBuilders; private void showFancyMessage(String message) { FancyMessageScreen screen = screenBuilders.screen(this) .withScreenClass(FancyMessageScreen.class) .build(); screen.setFancyMessage(message); screen.show(); }
另一个方式是为参数定义一个特殊的类,然后在界面构造器中将该类的实例传递给标准的
withOptions()
方法。参数类必需实现ScreenOptions
标记接口。示例:import com.haulmont.cuba.gui.screen.ScreenOptions; public class FancyMessageOptions implements ScreenOptions { private String message; public FancyMessageOptions(String message) { this.message = message; } public String getMessage() { return message; } }
在打开的
FancyMessageScreen
界面,可以通过InitEvent和AfterInitEvent处理器获取参数:@Subscribe private void onInit(InitEvent event) { ScreenOptions options = event.getOptions(); if (options instanceof FancyMessageOptions) { String message = ((FancyMessageOptions) options).getMessage(); messageLabel.setValue(message); } }
带有传递
ScreenOptions
参数使用ScreenBuilders
来调用FancyMessageScreen
如下所示:@Inject private ScreenBuilders screenBuilders; private void showFancyMessage(String message) { screenBuilders.screen(this) .withScreenClass(FancyMessageScreen.class) .withOptions(new FancyMessageOptions(message)) .build() .show(); }
可以看到,这个方式需要在控制界接收参数的时候进行类型转换,所以需要考虑清楚再使用。推荐还是用上面介绍的类型安全的使用 setter 的方式。
如果界面是基于legacy API从另一个界面打开的,那么使用
ScreenOptions
对象是唯一能获取到参数的方法。此时,参数对象是MapScreenOptions
类型的,可以在打开的界面中按照如下处理:@Subscribe private void onInit(InitEvent event) { ScreenOptions options = event.getOptions(); if (options instanceof MapScreenOptions) { String message = (String) ((MapScreenOptions) options).getParams().get("message"); messageLabel.setValue(message); } }
- 关闭界面后执行代码以及关闭界面返回值
-
每个界面在关闭时都会发送
AfterCloseEvent
事件。可以为界面添加监听器,这样可以在界面关闭时收到通知,示例:@Inject private Screens screens; @Inject private Notifications notifications; private void openOtherScreen() { OtherScreen otherScreen = screens.create(OtherScreen.class); otherScreen.addAfterCloseListener(afterCloseEvent -> { notifications.create().withCaption("Closed " + afterCloseEvent.getScreen()).show(); }); otherScreen.show(); }
当使用
ScreenBuilders
时,可以在withAfterCloseListener()
方法中提供监听器:@Inject private ScreenBuilders screenBuilders; @Inject private Notifications notifications; private void openOtherScreen() { screenBuilders.screen(this) .withScreenClass(OtherScreen.class) .withAfterCloseListener(afterCloseEvent -> { notifications.create().withCaption("Closed " + afterCloseEvent.getScreen()).show(); }) .build() .show(); }
事件对象能提供关于界面是如何关闭的信息:其
getCloseAction()
方法返回带CloseAction
接口的对象。界面控制器实现的FrameOwner
接口包含几个常量,定义了框架使用的CloseAction
接口的实现。在应用程序中,可以直接使用这些常量或者创建自己的实现。看一个简单的自定义界面:
package com.company.demo.web.screens; import com.haulmont.cuba.gui.components.Button; import com.haulmont.cuba.gui.screen.*; @UiController("demo_OtherScreen") @UiDescriptor("other-screen.xml") public class OtherScreen extends Screen { private String result; public String getResult() { return result; } @Subscribe("okBtn") private void onOkBtnClick(Button.ClickEvent event) { result = "Done"; close(WINDOW_COMMIT_AND_CLOSE_ACTION); (1) } @Subscribe("cancelBtn") private void onCancelBtnClick(Button.ClickEvent event) { closeWithDefaultAction(); (2) } }
1 - 在点击 "OK" 按钮时,设置一些结果状态,并使用标准的 WINDOW_COMMIT_AND_CLOSE_ACTION
操作关闭界面。2 - 在点击 "Cancel" 按钮时,使用默认操作关闭界面。 于是,在
AfterCloseEvent
监听器我们能分析界面是如何关闭的,并且如果需要的话可以读取结果:@Inject private ScreenBuilders screenBuilders; @Inject private Notifications notifications; private void openOtherScreen() { screenBuilders.screen(this) .withScreenClass(OtherScreen.class) .withAfterCloseListener(afterCloseEvent -> { OtherScreen otherScreen = afterCloseEvent.getScreen(); if (afterCloseEvent.getCloseAction().equals(WINDOW_COMMIT_AND_CLOSE_ACTION)) { String result = otherScreen.getResult(); notifications.create().withCaption("Result: " + result).show(); } }) .build() .show(); }
从界面返回值的另一个方法是使用自定义的
CloseAction
实现。重写一下上面的示例,使用如下操作类:package com.company.demo.web.screens; import com.haulmont.cuba.gui.screen.StandardCloseAction; public class MyCloseAction extends StandardCloseAction { private String result; public MyCloseAction(String result) { super("myCloseAction"); this.result = result; } public String getResult() { return result; } }
然后可以使用该操作类关闭界面:
@Inject private Screens screens; @Inject private Notifications notifications; private void openOtherScreen() { Screen otherScreen = screens.create("demo_OtherScreen", OpenMode.THIS_TAB); otherScreen.addAfterCloseListener(afterCloseEvent -> { CloseAction closeAction = afterCloseEvent.getCloseAction(); if (closeAction instanceof MyCloseAction) { String result = ((MyCloseAction) closeAction).getResult(); notifications.create().withCaption("Result: " + result).show(); } }); otherScreen.show(); }
可以看到,当使用自定义的
CloseAction
返回值时,调用方不需要知道打开的界面类是什么,因为不会调用具体的界面控制器内的方法。所以界面可以只通过其字符串 id 来创建。当然,在使用
ScreenBuilders
打开界面时,也可以使用相同的方式通过关闭操作返回结果。
3.5.1.4. 使用界面 Fragments
在本章节,介绍如何定义和使用界面 fragments。
- 声明式使用 fragment
-
假设我们有用来输入地址的 fragment:
AddressFragment.java@UiController("demo_AddressFragment") @UiDescriptor("address-fragment.xml") public class AddressFragment extends ScreenFragment { }
address-fragment.xml<fragment xmlns="http://schemas.haulmont.com/cuba/screen/fragment.xsd"> <layout> <textField id="cityField" caption="City"/> <textField id="zipField" caption="Zip"/> </layout> </fragment>
然后我们可以在其它界面使用
fragment
元素来包含此 fragment,fragment
元素需要有指向 fragment id 的screen
属性,fragment id 在其@UiController
注解设置:host-screen.xml<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd" caption="Some Screen"> <layout> <groupBox id="addressBox" caption="Address"> <fragment screen="demo_AddressFragment"/> </groupBox> </layout> </window>
fragment
元素可以添加在界面任意的 UI 容器中,也包含最顶上的layout
元素。
- 编程式使用 fragment
-
同一个 fragment也可以通过编程的方式添加到界面,需要在 InitEvent 或 AfterInitEvent 事件处理器中添加:
host-screen.xml<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd" caption="Some Screen"> <layout> <groupBox id="addressBox" caption="Address"/> </layout> </window>
HostScreen.java@UiController("demo_HostScreen") @UiDescriptor("host-screen.xml") public class HostScreen extends Screen { @Inject private Fragments fragments; (1) @Inject private GroupBoxLayout addressBox; @Subscribe private void onInit(InitEvent event) { AddressFragment addressFragment = fragments.create(this, AddressFragment.class); (2) addressFragment.init(); (3) addressBox.add(addressFragment.getFragment()); (4) } }
1 - 注入 Fragments
bean,用来实例化界面 fragment2 - 用 class 创建 fragment 控制器 3 - 初始化界面 fragment 4 - 从控制器获取 Fragment
可视化组件,并添加到 UI 容器别忘了在用编程的方式创建 fragment 之后需要使用
init()
方法进行初始化。如果 fragment 带有参数,可以在调用init()
之前使用 public setters 进行设置。之后,fragment 控制器的InitEvent
和AfterInitEvent
处理方法里面可以访问到这些参数。
- 界面 fragment 中的数据组件
-
界面 fragment 可以有自己的数据容器和数据加载器,通过 XML 元素
data
定义。同时,框架会为界面及其所有 fragments 创建DataContext的单例。因此,所有加载的实体都合并到同一数据上下文,并在父界面提交的时候一起保存更改。下面的例子中,会使用界面 fragment 自己的数据容器和加载器。
假设在 fragment 中有
City
实体,我们希望使用下拉列表框展示可选的城市而不仅仅用文本控件来展示。可以跟普通界面一样,在 fragment 的 XML 描述中定义数据组件。address-fragment.xml<fragment xmlns="http://schemas.haulmont.com/cuba/screen/fragment.xsd"> <data> <collection id="citiesDc" class="com.company.demo.entity.City" view="_base"> <loader id="citiesLd"> <query><![CDATA[select e from demo_City e ]]></query> </loader> </collection> </data> <layout> <lookupField id="cityField" caption="City" optionsContainer="citiesDc"/> <textField id="zipField" caption="Zip"/> </layout> </fragment>
如果需要在父界面打开时加载 fragment 的数据,需要订阅父界面事件:
AddressFragment.java@UiController("demo_AddressFragment") @UiDescriptor("address-fragment.xml") public class AddressFragment extends ScreenFragment { @Inject private CollectionLoader<City> citiesLd; @Subscribe(target = Target.PARENT_CONTROLLER) (1) private void onBeforeShowHost(Screen.BeforeShowEvent event) { citiesLd.load(); } }
1 - 订阅父界面的 BeforeShowEvent 事件 @LoadDataBeforeShow
对界面 fragments 无效。
- 使用已有的数据容器
-
下一个例子展示了如何在 fragment 中使用父界面的数据容器。
host-screen.xml<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd" caption="Some Screen"> <data> <instance id="addressDc" class="com.company.demo.entity.Address"/> (1) </data> <layout> <groupBox id="addressBox" caption="Address"> <fragment screen="demo_AddressFragment"/> </groupBox> </layout> </window>
1 - fragment 将在下面使用的数据容器 address-fragment.xml<fragment xmlns="http://schemas.haulmont.com/cuba/screen/fragment.xsd"> <data> <instance id="addressDc" class="com.company.demo.entity.Address" provided="true"/> (1) <collection id="citiesDc" class="com.company.demo.entity.City" view="_base"> <loader id="citiesLd"> <query><![CDATA[select e from demo_City e]]></query> </loader> </collection> </data> <layout> <lookupField id="cityField" caption="City" optionsContainer="citiesDc" dataContainer="addressDc" property="city"/> (2) <textField id="zipField" caption="Zip" dataContainer="addressDc" property="zip"/> </layout> </fragment>
1 - provided="true"
表示使用同样 id 的容器必须存在于父界面或者嵌套的 fragment 中,也就是说必须在该 fragment 外部提供2 - UI 组件连接到提供的数据容器 在包含
provided="true"
属性的 XML 元素中,除了id
之外其它所有的属性都会被忽略,但是也可以加上,以便提供设计思路。
- 订阅父界面事件
-
在 fragment 控制器中,可以订阅父界面的事件,需要在注解中为
target
属性指定PARENT_CONTROLLER
值,示例:@Subscribe(target = Target.PARENT_CONTROLLER) private void onBeforeShowHost(Screen.BeforeShowEvent event) { // }
这个方法可以处理任何事件,包括实体编辑界面发送的 InitEntityEvent 事件。
3.5.1.5. 界面 Mixins
通过 Mixin 可以创建能在多个UI界面中重复使用的功能,而且不需要从公共基类继承界面。Mixin 通过 Java 接口实现,使用了接口的默认方法。
Mixin 有如下特性:
-
一个界面可以有多个 Mixin。
-
Mixin 接口可以订阅 界面事件。
-
Mixin 可以在界面中保存一些状态,如果需要的话。
-
Mixin 也可以获取界面组件和基础架构 bean,比如 Dialogs,Notifications 等。
-
如果需要参数化 mixin 的行为,mixin 可以依赖界面的注解或者引入抽象方法交由界面实现。
使用 mixin 与在界面控制器中实现特定的接口一样简单。下面的示例中,CustomerEditor
界面使用了由 HasComments
,HasHistory
,HasAttachments
接口实现的 mixin 功能:
public class CustomerEditor extends StandardEditor<Customer>
implements HasComments, HasHistory, HasAttachments {
// ...
}
Mixin 可以使用以下类来处理界面和界面基础架构:
-
com.haulmont.cuba.gui.screen.Extensions
提供静态方法,用来保存和获取 mixin 使用的界面状态,还能访问BeanLocator
,这可以用来获取任何 Spring 管理的 bean。 -
UiControllerUtils
提供对界面UI和数据组件的访问。
下面是展示如何创建和使用 mixin 的示例。
- DeclarativeLoaderParameters mixin
-
下面这个 mixin 可以帮助在数据容器之间创建主从关系。通常的做法,是需要订阅主容器的
ItemChangeEvent
事件,将改动的主容器内容设置为从容器的数据加载器参数,如数据组件之间的依赖所述。但是如果参数是指向主容器的特殊名称,mixin 能自动完成此功能。Mixin 会使用状态对象在事件处理器之间传递信息。这里为了演示,我们将逻辑分散开,但实际上所有的逻辑都可以在一个
BeforeShowEvent
处理器中完成。首先,为共享状态创建一个类。包含单一字段,用来保存将在
BeforeShowEvent
处理器中触发的一组数据加载器:package com.company.demo.web.mixins; import com.haulmont.cuba.gui.model.DataLoader; import java.util.Set; public class DeclarativeLoaderParametersState { private Set<DataLoader> loadersToLoadBeforeShow; public DeclarativeLoaderParametersState(Set<DataLoader> loadersToLoadBeforeShow) { this.loadersToLoadBeforeShow = loadersToLoadBeforeShow; } public Set<DataLoader> getLoadersToLoadBeforeShow() { return loadersToLoadBeforeShow; } }
接下来,创建 mixin 接口:
package com.company.demo.web.mixins; import com.haulmont.cuba.gui.model.*; import com.haulmont.cuba.gui.screen.*; import java.util.HashSet; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; public interface DeclarativeLoaderParameters { Pattern CONTAINER_REF_PATTERN = Pattern.compile(":(container\\$(\\w+))"); @Subscribe default void onDeclarativeLoaderParametersInit(Screen.InitEvent event) { (1) Screen screen = event.getSource(); ScreenData screenData = UiControllerUtils.getScreenData(screen); (2) Set<DataLoader> loadersToLoadBeforeShow = new HashSet<>(); for (String loaderId : screenData.getLoaderIds()) { DataLoader loader = screenData.getLoader(loaderId); String query = loader.getQuery(); Matcher matcher = CONTAINER_REF_PATTERN.matcher(query); while (matcher.find()) { (3) String paramName = matcher.group(1); String containerId = matcher.group(2); InstanceContainer<?> container = screenData.getContainer(containerId); container.addItemChangeListener(itemChangeEvent -> { (4) loader.setParameter(paramName, itemChangeEvent.getItem()); (5) loader.load(); }); if (container instanceof HasLoader) { (6) loadersToLoadBeforeShow.add(((HasLoader) container).getLoader()); } } } DeclarativeLoaderParametersState state = new DeclarativeLoaderParametersState(loadersToLoadBeforeShow); (7) Extensions.register(screen, DeclarativeLoaderParametersState.class, state); } @Subscribe default void onDeclarativeLoaderParametersBeforeShow(Screen.BeforeShowEvent event) { (8) Screen screen = event.getSource(); DeclarativeLoaderParametersState state = Extensions.get(screen, DeclarativeLoaderParametersState.class); for (DataLoader loader : state.getLoadersToLoadBeforeShow()) { loader.load(); (9) } } }
1 - 订阅 InitEvent。 2 - 获取 ScreenData
对象,其中注册了 XML 中定义的所有数据容器和加载器。3 - 检查加载器的参数是否符合 :container$masterContainerId
模式的定义。4 - 从参数名中抽取主容器id,然后为该容器注册一个 ItemChangeEvent
监听器。5 - 使用新的主实体重新加载从实体数据加载器。 6 - 将主加载器添加到集合中,以便之后在 BeforeShowEvent
处理器中能触发。7 - 创建共享状态对象,使用 Extensions
工具类将该对象保存在界面中。8 - 订阅 BeforeShowEvent 事件。 9 - 触发在 InitEvent
处理器中找到的所有主加载器。在界面 XML 描述中定义主从容器以及数据加载器。从加载器需要带有一个参数,其名称类似
:container$masterContainerId
:<collection id="countriesDc" class="com.company.demo.entity.Country" view="_local"> <loader id="countriesDl"> <query><![CDATA[select e from demo_Country e]]></query> </loader> </collection> <collection id="citiesDc" class="com.company.demo.entity.City" view="city-view"> <loader id="citiesDl"> <query><![CDATA[ select e from demo_City e where e.country = :container$countriesDc ]]></query> </loader> </collection>
在界面控制器中,只需要添加 mixin 接口,然后就能自动触发加载器了:
package com.company.demo.web.country; import com.company.demo.entity.Country; import com.company.demo.web.mixins.DeclarativeLoaderParameters; import com.haulmont.cuba.gui.screen.*; @UiController("demo_Country.browse") @UiDescriptor("country-browse.xml") @LookupComponent("countriesTable") public class CountryBrowse extends StandardLookup<Country> implements DeclarativeLoaderParameters { }
3.5.2. 可视化组件库
3.5.2.1. 可视化组件
菜单 |
|
按钮 |
|
文本 |
|
文本输入 |
|
日期输入 |
|
选择 |
|
上传 |
|
表格和树 |
|
其它 |
|
3.5.2.1.1. 应用程序菜单
AppMenu
应用程序菜单组件提供了在主窗口布局中自定义主菜单的方式,通过它可以动态管理菜单项。
CUBA 框架提供标准的 mainWindow
screen - 主窗口界面;CUBA Studio 也基于这个主窗口界面提供了界面模板。这个模板扩展了 AppMainWindow
类,通过它可以直接访问 AppMenu
- 应用程序菜单实例:
public class ExtAppMainWindow extends AppMainWindow {
@Inject
private Notifications notifications;
@Subscribe
protected void onInit(InitEvent event) {
AppMenu.MenuItem item = mainMenu.createMenuItem("shop", "Shop");
AppMenu.MenuItem subItem = mainMenu.createMenuItem("customer", "Customers", null, menuItem -> {
notifications.create()
.withCaption("Customers menu item clicked")
.withType(Notifications.NotificationType.HUMANIZED)
.show();
});
item.addChildItem(subItem);
mainMenu.addMenuItem(item, 0);
}
}
AppMenu
- 应用程序菜单接口的方法有:
-
addMenuItem()
- 往根菜单列表末尾指定位置添加菜单项目。
-
createMenuItem()
- 是一个创建新菜单项目的工厂方法。并不会把菜单项目添加到菜单里。id
必须在菜单中唯一。
-
createSeparator()
- 创建菜单分隔线。 -
getMenuItem()/getMenuItemNN()
- 根据菜单树中的id
返回菜单项目。 -
getMenuItems()
- 返回菜单根菜单项列表。 -
hasMenuItems()
- 如果该菜单包含菜单项则返回true
。
MenuItem
- 菜单项接口的方法有:
-
addChildItem() / removeChildItem()
- 在子菜单项列表末尾或者指定位置添加/删除菜单项。 -
getCaption()
- 返回菜单项标题。 -
getChildren()
- 返回子菜单项列表。
-
setCommand()
- 设置菜单项命令或点击菜单项目需要执行的动作。 -
setDescription()
- 设置菜单项的描述,会在弹出提示中显示。 -
setIconFromSet()
- 设置菜单项的图标。 -
getId()
- 返回菜单项的 id。 -
getMenu()
- 返回菜单项所属菜单。 -
setStylename()
- 设置一个或多个自定义的样式,会替换已有的所有自定义样式。多个样式之间用空格分开。样式名称必须是有效的 CSS 类名称。 -
hasChildren()
- 该菜单项有子菜单项时返回true
。 -
isSeparator()
- 如果该菜单项是分隔线的话返回true
。 -
setVisible()
- 设置菜单项是否可见。
- API
3.5.2.1.2. 浏览器框架
BrowserFrame
是用来显示嵌入的网页,跟 HTML 里面的 iframe
元素的效果是一样的。
该组件对应的 XML 名称: browserFrame
下面是界面 XML 描述中这个组件定义的一个例子:
<browserFrame id="browserFrame"
height="280px"
width="600px"
align="MIDDLE_CENTER">
<url url="https://www.cuba-platform.com/blog/cuba-7-the-new-chapter"/>
</browserFrame>
跟 Image 组件类似, BrowserFrame
组件也可以显示不同来源的图片。可以用下面提到的 browserFrame
的 XML 元素来声明式的设置图片来源的类型:
-
classpath
- classpath 中能访问的资源。<browserFrame> <classpath path="com/company/sample/web/screens/myPic.jpg"/> </browserFrame>
-
file
- 文件系统的资源。<browserFrame> <file path="D:\sample\modules\web\web\VAADIN\images\myImage.jpg"/> </browserFrame>
-
relativePath
- 应用程序目录中的资源。<browserFrame> <relativePath path="VAADIN/images/myImage.jpg"/> </browserFrame>
-
theme
- 主题中用到的资源,比如:<browserFrame> <theme path="../halo/com.company.demo/myPic.jpg"/> </browserFrame>
-
url
- 可从指定 URL 加载的资源。<browserFrame> <url url="http://www.foobar2000.org/"/> </browserFrame>
browserFrame
的属性:
-
alternateText
- 如果 frame 中没有设置内容源或者内容源不可用的情况下,作为默认显示的文字。
browserFrame
内容源的配置信息:
-
bufferSize
- 下载资源时的缓存大小,以字节为单位。<browserFrame> <file bufferSize="1024" path="C:/img.png"/> </browserFrame>
-
cacheTime
- 缓存的失效时间,以毫秒为单位.<browserFrame> <file cacheTime="2400" path="C:/img.png"/> </browserFrame>
-
mimeType
- 资源的 MIME 类型。<browserFrame> <url url="https://avatars3.githubusercontent.com/u/17548514?v=4&s=200" mimeType="image/png"/> </browserFrame>
BrowserFrame
定义的接口方法:
-
addSourceChangeListener()
- 添加一个监听器,当 frame 的内容源发生变化时触发。@Inject private Notifications notifications; @Inject BrowserFrame browserFrame; @Subscribe protected void onInit(InitEvent event) { browserFrame.addSourceChangeListener(sourceChangeEvent -> notifications.create() .withCaption("Content updated") .show()); }
-
setSource()
- 设置 frame 的内容源。这个方法接受一个描述资源类型的参数,然后返回相应的资源对象,以便可以继续使用流式操作对资源进行更多的设置。每种资源类型有其独特的设置方法,比如,ThemeResource
具有的setPath()
方法、StreamResource
具有setStreamSupplier()
方法等:BrowserFrame frame = uiComponents.create(BrowserFrame.NAME); try { frame.setSource(UrlResource.class).setUrl(new URL("http://www.foobar2000.org/")); } catch (MalformedURLException e) { throw new RuntimeException(e); }
也可以使用用于
Image
组件的资源类型。
-
createResource()
- 用资源类型自己实现的方法创建资源对象,然后这个对象可以传递给setSource()
方法使用。UrlResource resource = browserFrame.createResource(UrlResource.class) .setUrl(new URL(fromString)); browserFrame.setSource(resource);
- 在 BrowserFrame 中使用 HTML 标记:
-
BrowserFrame
可以集成 HTML 标记到应用程序中。比如,可以在运行时根据用户的输入生成 HTML 内容。<textArea id="textArea" height="250px" width="400px"/> <browserFrame id="browserFrame" height="250px" width="500px"/>
textArea.addTextChangeListener(event -> { byte[] bytes = event.getText().getBytes(StandardCharsets.UTF_8); browserFrame.setSource(StreamResource.class) .setStreamSupplier(() -> new ByteArrayInputStream(bytes)) .setMimeType("text/html"); });
- 用 BrowserFrame 预览 PDF:
-
除了 HTML,
BrowserFrame
还可以用来展示 PDF 文件的内容。需要配置使用文件类型的资源和相应的 MIME 类型:@Inject private BrowserFrame browserFrame; @Inject private Resources resources; @Subscribe protected void onInit(InitEvent event) { browserFramePdf.setSource(StreamResource.class) .setStreamSupplier(() -> resources.getResourceAsStream("/com/company/demo/" + "web/screens/CUBA_Hands_on_Lab_6.8.pdf")) .setMimeType("application/pdf"); }
- BrowserFrame 的属性
-
align - alternateText - caption - captionAsHtml - contextHelpText - contextHelpTextHtmlEnabled - colspan - css - description - descriptionAsHtml - enable - box.expandRatio - height - icon - id - responsive - rowspan - stylename - visible - width
- BrowserFrame 资源的属性
- browserFrame 的 XML 元素
-
classpath - file - relativePath - theme - url
- API
3.5.2.1.3. 按钮
当用户点击一个按钮,就会执行一个操作。
该组件对应的 XML 名称: button
按钮上可以有标题、图标、或者两者皆有。下面这个图列举了一些不同类型的按钮。
下面是从本地化消息包获取文本显示到按钮和提示上的例子:
<button id="textButton" caption="msg://someAction" description="Press me"/>
按钮上的标题是用 caption 属性来设置,弹出提示用 description 来设置。
如果 disableOnClick
属性设置成 true
这个按钮在点击之后就会变成不可点击的状态,主要用来防止多次(意外的)点击这个按钮。之后可以通过调用 setEnabled(true)
把按钮恢复成可点击状态。
创建带有图标的按钮的例子:
<button id="iconButton" caption="" icon="SAVE"/>
按钮的主要功能是在点击的时候执行一个动作(action)。点击之后调用的控制器方法可以通过 invoke
属性来定义。这个属性的值需要是控制器的方法名,这个方法需要满足下面的条件:
-
方法应该是
public
。 -
方法返回值是
void
。 -
方法不能有任何参数, 或者只能有一个
Component
组件类型的参数。 如果方法带有Component
参数, 那么这个组件就是调用此方法的按钮实例。
以下是按钮调用 someMethod
的例子:
<button invoke="someMethod" caption="msg://someButton"/>
在界面控制器里需要定义名称为 someMethod
的方法:
public void someMethod() {
//some actions
}
如果设置了 action
属性,那么就会忽略 invoke
属性。action
属性包含了按钮中相应操作的名称。
带有 action
属性的按钮的例子:
<actions>
<action id="someAction" caption="msg://someAction"/>
</actions>
<layout>
<button action="someAction"/>
</layout>
实现了 Component.ActionsHolder
接口的组件中的任何操作都可以指定给按钮。表格、 分组表格、 树形表格、 树中的操作都可以指定给按钮。有两种添加操作的方法,一种是在 XML 描述中以声明的方式添加,另一种是在界面控制器里以编程的方式添加,这两种方式没有区别。不管使用哪种方式,在使用操作的时候,组件的名称和操作的标识符必须定义在 action
属性中,并且它们之间用 .
分隔。比如,下面的例子中,将 coloursTable
的 create
操作指定给一个按钮:
<button action="coloursTable.create"/>
按钮的操作也可以通过编程创建,方法是在界面控制器中创建继承自BaseAction的类。
如果给 如果 |
- 按钮样式
-
primary
属性用来将按钮设置为高亮显示,默认情况下,如果这个按钮调用的操作的primary属性为true
,这个按钮会被设置为高亮显示。<button primary="true" invoke="foo"/>
这个高亮样式在 Hover 主题中是默认开启的;如果希望在 Halo-based 主题中使用 primary 样式,可以通过设置
$cuba-highlight-primary-action
样式变量为true
来开启。接下来,在使用了 Halo-based 主题的 Web Client 里,可以通过
stylename
属性来给按钮组件设置一些预定义的样式,可以通过 XML 或者编程的方法设置:<button id="button" caption="Friendly button" stylename="friendly"/>
如果使用编程的方式来设置样式, 可以直接用
HaloTheme
主题类里面的以BUTTON_
开头的一些主题常量:button.setStyleName(HaloTheme.BUTTON_FRIENDLY);
- 按钮的属性
-
action - align - caption - captionAsHtml - css - description - descriptionAsHtml - disableOnClick - enable - box.expandRatio - icon - id - invoke - stylename - tabIndex - visible - width
- 按钮预定义的样式
-
borderless - borderless-colored - danger - friendly - huge - icon-align-right - icon-align-top - icon-only - large - primary - quiet - small - tiny
3.5.2.1.4. 批量编辑器
该组件对应的 XML 名称: bulkEditor
要使用 BulkEditor
, 相应的表格或者树组件的 multiselect
属性需设置为 "true"
。
批量实体编辑界面是基于定义的 view(view 里一般包括实体的字段和引用)、实体动态属性和用户权限自动生成的。系统属性不会显示在生成的界面里。
实体属性名称会按字母排序。默认情况下,值都为空,界面提交的时候,非空值会更新到所有的实体对象中。
批量实体编辑界面也支持批量删除值 - 实体对象的对应字段会设置为空( null
)。操作方法是点击字段旁边的 按钮,点击之后,该字段变为不可编辑, 再次点击该按钮则该字段恢复可编辑。
以下为在表格中使用 bulkEditor
批量编辑器的例子:
<table id="invoiceTable"
multiselect="true"
width="100%">
<actions>
<!-- ... -->
</actions>
<buttonsPanel>
<!-- ... -->
<bulkEditor for="invoiceTable"
exclude="customer"/>
</buttonsPanel>
-
bulkEditor
批量编辑器的属性有 -
-
属性
exclude
标识需要在批量编辑界面排除的字段,它可以包含一个正则表达式。比如:date|customer
-
includeProperties
- 定义批量编辑界面需要包含的字段;设置它以后,其它字段会被忽略。includeProperties
不会应用到动态属性。以声明的方式设置时,多个属性之间应该用逗号隔开:
<bulkEditor for="ordersTable" includeProperties="name, description"/>
这些属性也可以在界面控制器中以编程的方式设置:
bulkEditor.setIncludeProperties(Arrays.asList("name", "description"));
-
loadDynamicAttributes
定义实体的动态属性是否在批量编辑界面显示。默认为true
。
-
useConfirmDialog
定义保存之前是否弹出确认对话框,默认为true
。
-
- bulkEditor 批量编辑器的属性
-
align - caption - captionAsHtml - css - description - descriptionAsHtml - enable - exclude - box.expandRatio - for - icon - id - includeProperties - loadDynamicAttributes - openType - stylename - tabIndex - useConfirmDialog - visible - width
3.5.2.1.5. 日历控件
Calendar
- 日历控件用来组织和展示日历事件。
该组件的 XML 名称是: calendar
以下为界面中日历组件定义的示例:
<calendar id="calendar"
captionProperty="caption"
startDate="2016-10-01"
endDate="2016-10-31"
height="100%"
width="100%"/>
该组件在界面上的外观取决于日历的日期范围,日期范围由开始日期和结束日期定义。默认外观为周视图,对应的日期范围上限是一周七天。如果需要按天展示则把日期范围定义为一天以内。如果日期范围在七天以上,该组件将以月视图展示。
用来将日历控件往前或往后调一周的导航按钮默认不可见。可以通过设置属性 navigationButtonsVisible
使导航按钮可见(在周视图中):
<calendar width="100%"
height="100%"
navigationButtonsVisible="true"/>
calendar
日历控件的属性:
-
endDate
- 日期范围中的结束日期。
-
endDateProperty
- 包含结束日期的实体属性名称。
-
descriptionProperty
- 包含事件描述的实体属性名称。
-
isAllDayProperty
- 标识是否为全天事件的实体属性名称。
-
startDate
- 日期范围中的开始日期。
-
startDateProperty
- 包含开始时间的是实体属性名称。
-
stylenameProperty
- 标识事件样式名称的实体属性名称。
-
timeFormat
- 时间格式: 12H 或 24H。- 如何使用日历事件:
-
需要在日历控件单元格中显示事件时,可以通过
addEvent()
方法添加一个事件到Calendar
对象;也可以使用CalendarEventProvider
接口操作。 直接添加事件到对象的示例:@Inject private Calendar calendar; public void generateEvent(String caption, String description, Date start, Date end, boolean isAllDay, String stylename) { SimpleCalendarEvent calendarEvent = new SimpleCalendarEvent(); calendarEvent.setCaption(caption); calendarEvent.setDescription(description); calendarEvent.setStart(start); calendarEvent.setEnd(end); calendarEvent.setAllDay(isAllDay); calendarEvent.setStyleName(stylename); calendar.getEventProvider().addEvent(calendarEvent); }
CalendarEventProvider
接口的removeEvent()
方法可以通过指定索引删除事件:CalendarEventProvider eventProvider = calendar.getEventProvider(); List<CalendarEvent> events = new ArrayList<>(eventProvider.getEvents()); eventProvider.removeEvent(events.get(events.size()-1));
removeAllEvents
方法依次删除所有事件:CalendarEventProvider eventProvider = calendar.getEventProvider(); eventProvider.removeAllEvents();
有两个给日历控件添加事件数据的接口:
ListCalendarEventProvider
(默认创建) 和EntityCalendarEventProvider
。ListCalendarEventProvider
的addEvent()
方法需要CalendarEvent
对象做为输入参数:@Inject private Calendar calendar; public void addEvents() { ListCalendarEventProvider listCalendarEventProvider = new ListCalendarEventProvider(); calendar.setEventProvider(listCalendarEventProvider); listCalendarEventProvider.addEvent(generateEvent( "Training", "Student training", "2016-10-17 09:00", "2016-10-17 14:00", false, "event-blue")); listCalendarEventProvider.addEvent(generateEvent( "Development", "Platform development", "2016-10-17 15:00", "2016-10-17 18:00", false, "event-red")); listCalendarEventProvider.addEvent(generateEvent( "Party", "Party with friends", "2016-10-22 13:00", "2016-10-22 18:00", false, "event-yellow")); } private SimpleCalendarEvent generateEvent(String caption, String description, String start, String end, Boolean allDay, String style) { SimpleCalendarEvent calendarEvent = new SimpleCalendarEvent(); SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm"); calendarEvent.setCaption(caption); calendarEvent.setDescription(description); calendarEvent.setStart(df.parse(start)); calendarEvent.setEnd(df.parse(end)); calendarEvent.setAllDay(allDay); calendarEvent.setStyleName(style); return calendarEvent; }
EntityCalendarEventProvider
可以直接将事件实体对象添加为日历的事件。事件实体对象必须 至少 包含事件开始日期(DateTime 类型)、事件结束日期(DateTime 类型)和事件标题(String 类型)。以下示例中假设事件数据源包含了所有需要的属性:
eventCaption
、eventDescription
、eventStartDate
、eventEndDate
、eventStylename
, 他们的值将会设置到calendar
组件的属性中:<calendar id="calendar" datasource="calendarEventsDs" width="100%" height="100%" startDate="2016-10-01" endDate="2016-10-31" captionProperty="eventCaption" descriptionProperty="eventDescription" startDateProperty="eventStartDate" endDateProperty="eventEndDate" stylenameProperty="eventStylename"/>
Calendar
组件支持几个用来响应用户交互事件的监听器,比如日期、周标题,日期/事件范围选择,事件拖拽或者事件大小调整,往前或往后调日期的导航按钮。以下是监听器列表:
-
addDateClickListener(CalendarDateClickListener listener);
- 日期点击监听器:calendar.addDateClickListener( calendarDateClickEvent -> notifications.create() .withCaption(String.format("Date clicked: %s", calendarDateClickEvent.getDate().toString())) .show());
-
addWeekClickListener()
- 周点击监听器。
-
addEventClickListener()
- 日历事件点击监听器。
-
addEventResizeListener()
- 日历事件大小调整监听器。
-
addEventMoveListener()
- 事件拖拽、释放监听器。
-
addForwardClickListener()
- 往前调日期导航钮的监听器。
-
addBackwardClickListener()
- 往后调日期导航钮的监听器。
-
addRangeSelectListener()
- 日历日期范围选择监听器。
日历事件可以通过 CSS 配置样式。需要配置 CSS 的样式时,创建一个样式名字,然后在 .scss
文件中设置需要的参数。例如,需要配置事件的背景颜色时:
.v-calendar-event.event-green {
background-color: #c8f4c9;
color: #00e026;
}
然后通过事件的 setStyleName
方法设置样式名字:
calendarEvent.setStyleName("event-green");
然后,事件的背景会被设置为绿色:
如果需要改变 Calendar
日历控件上日期和月份的名称,可以通过方法 setDayNames()
和 setMonthNames()
实现,传入带新名称的 map 即可:
Map<DayOfWeek, String> days = new HashMap<>(7);
days.put(DayOfWeek.MONDAY,"Heavens and earth");
days.put(DayOfWeek.TUESDAY,"Sky");
days.put(DayOfWeek.WEDNESDAY,"Dry land");
days.put(DayOfWeek.THURSDAY,"Stars");
days.put(DayOfWeek.FRIDAY,"Fish and birds");
days.put(DayOfWeek.SATURDAY,"Animals and man");
days.put(DayOfWeek.SUNDAY,"Rest");
calendar.setDayNames(days);
-
Calendar
日历控件的属性列表 -
caption - captionAsHtml - captionProperty - colspan - datasource - contextHelpText - contextHelpTextHtmlEnabled - css - description - descriptionAsHtml - descriptionProperty - endDateProperty - endDate - box.expandRatio - height - icon - id - isAllDayProperty - rowspan - startDate - startDateProperty - stylename - stylenameProperty - timeFormat - visible - width
- API
-
addEvent - removeAllEvents - removeEvent - setEventProvider - setDayNames
- 监听器
-
CalendarBackwardClickListener - CalendarDateClickListener - CalendarEventClickListener - CalendarEventMoveListener - CalendarEventResizeListener - CalendarForwardClickListener - CalendarRangeSelectListener - CalendarWeekClickListener
3.5.2.1.6. 大小写锁定提示器
这个组件在用户使用 PasswordField 输入密码的时候提示用户是否开启了大小写锁定。
该组件对应的 XML 名称: capsLockIndicator
可以用 capsLockOnMessage
和 capsLockOffMessage
属性来定义大小写锁定开启和关闭时的提示信息。
示例:
<hbox spacing="true">
<passwordField id="passwordField"
capsLockIndicator="capsLockIndicator"/>
<capsLockIndicator id="capsLockIndicator"/>
</hbox>
CapsLockIndicator capsLockIndicator = uiComponents.create(CapsLockIndicator.NAME);
capsLockIndicator.setId("capsLockIndicator");
passwordField.setCapsLockIndicator(capsLockIndicator);
CapsLockIndicator
组件被设计为配合 PasswordField 一起使用,当 PasswordField
获得焦点的时候该组件处理大小写锁定状态,当 PasswordField
失去焦点时,大小写锁定的状态就变成 inactive 了。因为此时没法监控关联的密码控件输入情况了。
可以用 visible 属性来动态改变 CapsLockIndicator
的可见性。但是如果此时界面已经打开了,控制可见性就不一定能按预想工作。
- capsLockIndicator 的属性
-
align - capsLockOffMessage - capsLockOnMessage - colspan - css - box.expandRatio - height - id - rowspan - stylename - visible - width
3.5.2.1.7. 复选框
CheckBox
是一个拥有两个状态的组件: 选中或者未选中。
该组件对应的 XML 名称: checkBox
通过本地化消息包获取标签的复选框例子:
<checkBox id="accessField" caption="msg://accessFieldCaption"/>
勾选或者取消勾选复选框会改变它的值为 Boolean.TRUE
或者 Boolean.FALSE
。这个值可以通过 getValue()
方法获取,也可以通过 setValue()
方法设置。通过 setValue()
设置 null
的话,会把值改成 Boolean.FALSE
然后取消复选框的选择。
复选框值的变化,跟其它组件一样,只要实现了 Field
接口,都可以通过 ValueChangeListener
监听到,ValueChangeEvent
事件的来源可以通过 isUserOriginated() 方法跟踪。比如:
@Inject
private CheckBox accessField;
@Inject
private Notifications notifications;
@Subscribe
protected void onInit(InitEvent event) {
accessField.addValueChangeListener(valueChangeEvent -> {
if (Boolean.TRUE.equals(valueChangeEvent.getValue())) {
notifications.create()
.withCaption("set")
.show();
} else {
notifications.create()
.withCaption("not set")
.show();
}
});
}
通过 dataContainer 和 property 属性可以创建关联数据源的复选框:
<data>
<instance id="customerDc" class="com.company.sales.entity.Customer" view="_local">
<loader/>
</instance>
</data>
<layout>
<checkBox dataContainer="customerDc" property="active"/>
</layout>
从这个例子可以看出,这个界面包含了一个关联 Customer
实体的 customerDs
数据容器,Customer 实体具有 active
属性。checkBox
的 dataContainer
属性需要指向一个数据容器;property
属性需要指向实体中需要展示到复选框控件的字段名字,这个字段必须要是 Boolean
类型。如果这个值是 null
,那么复选框是默认为非选中状态。
- 复选框组件的属性
-
align - caption - captionAsHtml - contextHelpText - contextHelpTextHtmlEnabled - css - dataContainer - datasource - description - descriptionAsHtml - editable - enable - box.expandRatio - height - icon - id - property - stylename - tabIndex - visible - width
- API
-
addValueChangeListener commit - discard - isModified - setContextHelpIconClickHandler
3.5.2.1.8. 颜色选择器
ColorPicker
- 颜色选择器可以让用户预览并选择一种颜色。以 String 类型返回所选颜色的十六进制值。
以下为颜色选择器示例,它的标题取自本地化消息包:
<colorPicker id="colorPicker" caption="msg://colorPickerCaption"/>
下图为一个颜色选择弹窗关闭状态下的颜色选择器示例。
如果需要将颜色选择器与数据关联,需要使用 dataContainer 和 property 属性。
<data>
<collection id="carsDc" class="com.company.sales.entity.Car" view="_local">
<loader>
<query>
<![CDATA[select e from sales_Car e]]>
</query>
</loader>
</collection>
</data>
<layout>
<colorPicker id="colorPicker" dataContainer="carsDc" property="color"/>
</layout>
colorPicker
- 颜色选择器的属性:
-
defaultCaptionEnabled
- 这个属性设置为true
并且buttonCaption
未设置时, 控件按钮上的标题为颜色的 HEX 值。
-
historyVisible
- 标识是否在弹出窗口显示最近选择过的颜色。
以下属性用来控制哪些标签页可见:
-
rgbVisible
- 控制 RGB 标签页是否可见。 -
hsvVisible
- 控制 HSV 标签页是否可见。 -
swatchesVisible
- 控制调色板标签页是否可见。
默认情况下只有 RGB 标签页可见。
-
popupCaption
- 弹出窗口标题。 -
confirmButtonCaption
- 确认按钮的标题。 -
cancelButtonCaption
- 取消按钮的标题。 -
swatchesTabCaption
- 调色板标签页标题。 -
lookupAllCaption
- 查找所有颜色的标题。 -
lookupRedCaption
- 查找红色的标题。 -
lookupGreenCaption
- 查找绿色的标题。 -
lookupBlueCaption
- 查找蓝色的标题。
getValue()
方法返回 String 类型,其中包含所选颜色的 HEX 码。
- colorPicker - 颜色选择器的属性列表
-
align - buttonCaption - cancelButtonCaption - caption - captionAsHtml - confirmButtonCaption - contextHelpText - contextHelpTextHtmlEnabled - css - dataContainer - datasource - defaultCaptionEnabled - description - descriptionAsHtml - editable - box.expandRatio - height - historyVisible - hsvVisible - icon - id - lookupAllCaption - lookupBlueCaption - lookupGreenCaption - lookupRedCaption - popupCaption - rgbVisible - required - stylename - swatchesTabCaption - swatchesVisible - tabIndex - visible - width
- API
3.5.2.1.9. 复选框组
这是一个允许用户使用复选框从选项列表中选择多个值的组件。
该组件对应的 XML 名称: checkBoxGroup
可以使用 setOptions()
、setOptionsList()
、setOptionsMap()
和 setOptionsEnum()
方法,或使用 optionsDatasource
或 optionsContainer
属性指定组件选项列表。
-
使用
CheckBoxGroup
的最简单的情况是为实体属性选择枚举值。例如,Role
实体具有RoleType
类型的type
属性,它是一个枚举。然后可以通过optionsEnum
属性来使用CheckBoxGroup
显示这个属性,如下所示:<checkBoxGroup optionsEnum="com.haulmont.cuba.security.entity.RoleType" property="type"/>
setOptionsEnum()
将一个枚举类作为参数。选项列表将包含枚举值的本地化名称,组件的值将是一个枚举值。@Inject private CheckBoxGroup<RoleType> checkBoxGroup; @Subscribe protected void onInit(InitEvent event) { checkBoxGroup.setOptionsEnum(RoleType.class); }
使用
setOptions()
方法可以得到相同的结果,该方法可以使用所有类型的选项:@Inject private CheckBoxGroup<RoleType> checkBoxGroup; @Subscribe protected void onInit(InitEvent event) { checkBoxGroup.setOptions(new EnumOptions<>(RoleType.class)); }
-
setOptionsList()
能够以编程方式指定组件选项列表。为此请在 XML 描述中声明一个组件:<checkBoxGroup id="checkBoxGroup"/>
然后将组件注入控制器并为其指定选项列表:
@Inject private CheckBoxGroup<Integer> checkBoxGroup; @Subscribe protected void onInit(InitEvent event) { List<Integer> list = new ArrayList<>(); list.add(2); list.add(4); list.add(5); list.add(7); checkBoxGroup.setOptionsList(list); }
该组件将如下所示:
根据所选的选项,组件的
getValue()
方法将返回Integer
类型的值:2、4、5、7。
-
setOptionsMap()
能够分别指定字符串名称和选项值。例如,我们可以为控制器中注入的checkBoxGroup
组件设置以下选项字典:@Inject private CheckBoxGroup<Integer> checkBoxGroup; @Subscribe protected void onInit(InitEvent event) { Map<String, Integer> map = new LinkedHashMap<>(); map.put("two", 2); map.put("four", 4); map.put("five", 5); map.put("seven", 7); checkBoxGroup.setOptionsMap(map); }
该组件将如下所示:
根据所选的选项,组件的
getValue()
方法将返回Integer
类型的值:2、4、5、7,而不是界面上显示的字符串。
-
该组件可以从数据容器中获取选项列表。要做到这点,需要使用
optionsContainer
属性。例如:<data> <collection id="employeesCt" class="com.company.demo.entity.Employee" view="_minimal"> <loader> <query><![CDATA[select e from demo_Employee e]]></query> </loader> </collection> </data> <layout> <checkBoxGroup optionsContainer="employeesCt"/> </layout>
在这种情况下,
checkBoxGroup
组件将显示位于employeesCt
数据容器中的Employee
实体的实例名,其getValue()
方法将返回所选实体实例。使用captionProperty属性,可以指定一个实体属性作为选项的显示名称,而不是使用实例名称作为选项的显示名称。
可以使用
CheckBoxGroup
接口的setOptions()
方法以编程方式定义选项容器:@Inject private CheckBoxGroup<Employee> checkBoxGroup; @Inject private CollectionContainer<Employee> employeesCt; @Subscribe protected void onInit(InitEvent event) { checkBoxGroup.setOptions(new ContainerOptions<>(employeesCt)); }
orientation
属性定义了组元素的方向。默认情况下,元素垂直排列。设置为 horizontal
将水平排列。
- CheckBoxGroup 的属性
-
align - box.expandRatio - caption - captionAsHtml - captionProperty - colspan - contextHelpText - contextHelpTextHtmlEnabled - css - dataContainer - datasource - description - descriptionAsHtml - editable - enable - height - icon - id - optionsContainer - optionsDatasource - optionsEnum - orientation - property - required - requiredMessage - responsive - rowspan - stylename - tabIndex - visible - width
- API
-
addValueChangeListener - setContextHelpIconClickHandler - setOptions - setOptionsEnum - setOptionsList - setOptionsMap
3.5.2.1.10. 货币组件
CurrencyField
是文本字段的子类型,专门用来输入货币值。在这个字段内部有个货币符号,默认是右对齐状态。
该组件对应的 XML 名称: currencyField
基本上,CurrencyField
跟 TextField 的功能是一样的。可以给这个字段手动设置一个数据类型,但是只支持从 NumericDatatype
继承的数字类型。如果提供的数据类型不能解析,程序会抛出异常。
CurrencyField
也可以通过 datasource
和 property
属性绑定数据源 :
<currencyField currency="$"
datasource="orderDs"
property="amount"/>
currencyField
属性:
-
currency
- 作为货币符号的文本。<currencyField currency="USD"/>
-
currencyLabelPosition
- 设置组件内货币符号的位置:-
LEFT
- 在组件文字输入的左侧 -
RIGHT
- 在组件文字输入的右侧(默认值)。
-
-
showCurrencyLabel
- 定义是否需要显示货币符号。
- currencyField 的属性
-
align - caption - captionAsHtml - colspan - contextHelpText - contextHelpTextHtmlEnabled - conversionErrorMessage - css - currency - currencyLabelPosition - dataContainer - datasource - datatype - description - descriptionAsHtml - editable - enable - box.expandRatio - height - icon - id - property - required - requiredMessage - rowspan - showCurrencyLabel - stylename - visible - width
- currencyField 预定义的样式
- API
-
addValidator - addValueChangeListener - commit - discard - isModified - setContextHelpIconClickHandler - setCurrency - setCurrencyLabelPosition - setShowCurrencyLabel
3.5.2.1.11. 数据网格
DataGrid
- 数据网格组件类似于表格组件, 适合用于展示、分类、排序表格类数据,由于使用了在滚动时加载数据的延迟加载方式,所以此组件具有更好的数据行、列操作性能。
该组件的 XML 名称为 dataGrid
以下为在 XML 文件中定义数据网格的示例:
<data>
<collection id="ordersDc" class="com.company.sales.entity.Order" view="order-with-customer">
<loader id="ordersDl">
<query>
<![CDATA[select e from sales_Order e order by e.date]]>
</query>
</loader>
</collection>
</data>
<layout>
<dataGrid id="ordersDataGrid" dataContainer="ordersDc" height="100%" width="100%">
<columns>
<column id="date" property="date"/>
<column id="customer" property="customer.name"/>
<column id="amount" property="amount"/>
</columns>
</dataGrid>
</layout>
其中<column>元素里的 id
属性标识一列,property
指示数据容器实体中的属性,对应的数据库中的数据会展示在该列。
dataGrid - 数据网格中的元素:
-
columns
- 必要元素,定义数据网格的所有列。其中每一列在单独内嵌的column
元素中定义,每一列的属性包括:-
id
- 非必须属性,标识一列。如果没有设置,对应的property
值会被用作该列的标识,所以这种时候property
值是必须的。否则会抛出GuiDevelopmentException
异常。如果列是在界面控制器代码中创建的,则id
属性是必须的。
-
property
- 指对应的实体属性。可以是数据源/数据容器实体的属性,也可以是关联实体的属性,关联实体属性前面需要加上关联类名字并通过“.”连接。例如:<columns> <column id="date" property="date"/> <column id="customer" property="customer"/> <column id="customerName" property="customer.name"/> <column id="customerCountry" property="customer.address.country"/> </columns>
-
caption
- 可选属性,定义列标题。如果未设置,对应的本地化属性名称会被做为列标题显示。
-
expandRatio
- 设置列宽占比。默认情况下,所有列等宽(expandRatio = 1
)。如果至少有一列设置了其它值,则忽略所有的隐式值,并且只使用设置的值。
-
collapsible
- 定义用户是否可以通过DataGrid
表格组件右上角的侧边栏菜单隐藏/显示该列。默认为true
。
-
collapsed
- 可选属性,设置为true
时自动隐藏该列。该属性默认值为false
。
-
collapsingToggleCaption
- 设置在侧边栏菜单中该列的标题。默认为null
, 此时侧边栏菜单中该列的标题与数据网格中该列的标题一致。
-
resizable
- 定义用户是否可以调整列宽。
-
sortable
- 可选属性,可以用来关闭针对该列的排序。当整个DataGrid
数据网格控件的sortable
属性设置为true
(默认值)时生效。 -
width
- 可选属性,定义列宽。只支持以像素为单位的数值类型。
-
minimumWidth
- 设置最小列宽,以像素为单位。
-
maximumWidth
- 设置最大列宽,以像素为单位。
<column id="date" property="date"> <formatter class="com.haulmont.cuba.gui.components.formatters.DateFormatter" format="yyyy-MM-dd HH:mm:ss" useUserTimezone="true"/> </column>
-
-
rowsCount
- 可选元素,会为数据网格控件创建一个行数(RowsCount
)控件。行数控件会启用数据分页,在界面控制器中调用CollectionLoader.setMaxResults()
方法可以控制数据加载器中的数据量,进而能控制每页最大行数。另外,绑定到相同数据源的通用过滤器组件也能实现此功能。
行数控件也会显示数据结果总数,但不需要把这些数据全部加载出来。用户可以点击 "?" 按钮, 它会调用 com.haulmont.cuba.core.global.DataManager#getCount
方法,该方法使用相同的参数请求数据库,同时使用 COUNT(*)
聚合函数代替加载数据。返回的数值会显示在 "?" 位置。
数据网格控件属性:
-
columnResizeMode
- 设置调整列宽时的动画效果。支持两种效果:-
ANIMATED
- 动画效果,列宽跟随鼠标拖拽(默认)。 -
SIMPLE
- 简单效果,列宽会在拖拽动作结束后才发生改变。
列宽变化事件可以通过监听器
ColumnResizeListener
跟踪。可以使用 isUserOriginated() 方法跟踪列宽变化事件的来源。 -
-
columnsCollapsingAllowed
- 允许隐藏/折叠列,定义用户是否可以在侧边栏菜单中隐藏/折叠某些列。侧边栏菜单中显示的列旁边会有复选框。当用户选择或者取消选择某列时,对应列的collapsed
属性值会变化。当columnsCollapsingAllowed
属性为false
时,列对应的collapsed
属性不能被设置为true
。列折叠状态的变化可以通过监听器
ColumnCollapsingChangeListener
跟踪。列折叠事件的来源可以使用 isUserOriginated() 方法进行跟踪.
-
contextMenuEnabled
- 开启或关闭右键菜单。默认为true
。DataGrid
数据网格控件的右键点击事件可以通过监听器ContextClickListener
跟踪。
-
editorBuffered
- 编辑器缓冲模式开启或关闭。默认为true
。
-
editorCancelCaption
- 设置DataGrid
数据网组件编辑器中取消(cancel)按钮的名称。
-
editorEnabled
- 启用数据项的行内编辑器。默认为false
。如果数据网格组件是跟 KeyValueCollectionContainer 或者 ValueCollectionDatasource 绑定的,则该数据是只读的,此时设置editorEnabled
属性便没有意义。
-
editorSaveCaption
- 设置数据网格组件编辑器中保存(save)按钮的名称。
-
frozenColumnCount
- 设置固定列的个数。0
表示不需要固定任何列,除了开启多选模式时的选择列。设为-1
的时候即使选择列也不固定。
-
headerVisible
- 定义是否显示表头。默认为true
。
-
reorderingAllowed
- 定义用户是否可以通过鼠标拖拽重新设置列的顺序。默认值为true
。列排序的改变事件可以通过监听器
ColumnReorderListener
跟踪。排序改变事件的来源可以通过 isUserOriginated() 方法跟踪。
-
selectionMode
- 设置行选择模式,支持以下四种:-
SINGLE
- 单行选择。 -
MULTI
- 多行选择。 -
MULTI_CHECK
- 通过内嵌复选框列进行多选。 -
NONE
- 不支持选择。行选中事件可以通过监听器
SelectionListener
跟踪。行选中事件的来源可以使用 isUserOriginated() 方法跟踪.
-
-
sortable
- 开启/关闭数据网格控件的排序功能。默认为true
。开启后,点击列名会在列名右边显示排序图标。使用列的sortable
属性可以禁用该列的排序功能。DataGrid
的排序事件可以通过监听器SortListener
跟踪。排序事件的来源可以通过 isUserOriginated() 方法跟踪。
-
textSelectionEnabled
- 开启/关闭数据网格单元格中的文字选择功能。默认为false
。
DataGrid 接口的方法:
-
getColumns()
- 按当前界面的展示顺序获取列集合。 -
getSelected()
、getSingleSelected()
- 返回所选行对应实体的实例。getSelected() 返回一个集合。如果没有选择任何行,则返回一个空的集合。如果设置的是SelectionMode.SINGLE
单选模式,用 getSingleSelected() 会更方便,它直接返回一个被选择的实体实例,或者 null(没有选择任何行)。 -
getVisibleColumns()
- 按当前界面中列的显示顺序获取用户可见的列集合。
-
scrollTo()
- 将DataGrid
滚动到指定行。需要一个实体实例做为输入参数来指定滚动到哪一行。除了实体实例参数,另有重载方法支持ScrollDestination
参数,该参数可以为以下值:-
ANY
- 滚动尽量少的位置来展示所需要的数据。 -
START
- 滚动DataGrid
,使所需要的数据展示在可见部分的顶端。 -
MIDDLE
- 滚动DataGrid
,使所需要的数据展示在可见部分的中部。 -
END
- 滚动DataGrid
,使所需要的数据展示在可见部分的底部。
-
-
scrollToStart()
andscrollToEnd()
- 将DataGrid
滚动到开头或结尾。
-
setCellStyleProvider()
- 设置DataGrid
单元格的显示样式。
-
setRowStyleProvider()
- 设置DataGrid
行显示样式。
-
setEnterPressAction()
- 设置按下 回车键 时需要执行的操作。如果没有定义这种操作,控件会尝试按以下顺序找一个合适的操作:-
setItemClickAction()
方法定义的操作。 -
通过
shortcut
快捷键属性定义给 回车键 的操作。 -
edit(编辑)
操作。 -
view(查看)
操作。
如果找到一个操作,并且其属性
enabled
=true
,则会执行它。 -
-
setItemClickAction()
- 设置双击时的操作。如果没有定义,组件会按以下顺序找一个合适的操作:-
通过
shortcut
快捷键属性定义给 回车键 的操作。 -
edit(编辑)
操作。 -
view(查看)
操作。
如果找到一个操作并且属性
enabled
=true
,则会执行它。单击事件可以通过监听器
ItemClickListener
跟踪。 -
-
sort()
- 根据指定列对数据进行排序,通过枚举值SortDirection
控制排序方式:-
ASCENDING
- 升序 (A-Z, 1..9)。 -
DESCENDING
- 降序 (Z-A, 9..1)。
-
使用描述提供者:
-
setDescriptionProvider()
方法用来为每个DataGrid
列的单元格生成可选的描述(提示)。描述支持 HTML 标记。@Inject private DataGrid<Customer> customersDataGrid; @Subscribe protected void onInit(InitEvent event) { customersDataGrid.getColumnNN("age").setDescriptionProvider(customer -> getPropertyCaption(customer, "age") + customer.getAge(), ContentMode.HTML); customersDataGrid.getColumnNN("active").setDescriptionProvider(customer -> getPropertyCaption(customer, "active") + getMessage(customer.getActive() ? "trueString" : "falseString"), ContentMode.HTML); customersDataGrid.getColumnNN("grade").setDescriptionProvider(customer -> getPropertyCaption(customer, "grade") + messages.getMessage(customer.getGrade()), ContentMode.HTML); }
-
setRowDescriptionProvider()
方法用来为每个DataGrid
行生成可选的描述(提示)。如果同时也设置了列描述提供者,只有在列描述提供者返回 null 时显示行描述提供器实例。customersDataGrid.setRowDescriptionProvider(Instance::getInstanceName);
使用 DetailsGenerator:
使用 setDetailsGenerator()
方法设置 DetailsGenerator
接口,可以生成自定义控件来展示对应行的明细:
@Inject
private DataGrid<Order> ordersDataGrid;
@Inject
private UiComponents uiComponents;
@Install(to = "ordersDataGrid", subject = "detailsGenerator")
protected Component ordersDataGridDetailsGenerator(Order order) {
VBoxLayout mainLayout = uiComponents.create(VBoxLayout.NAME);
mainLayout.setWidth("100%");
mainLayout.setMargin(true);
HBoxLayout headerBox = uiComponents.create(HBoxLayout.NAME);
headerBox.setWidth("100%");
Label infoLabel = uiComponents.create(Label.NAME);
infoLabel.setHtmlEnabled(true);
infoLabel.setStyleName("h1");
infoLabel.setValue("Order info:");
Component closeButton = createCloseButton(order); (1)
headerBox.add(infoLabel);
headerBox.add(closeButton);
headerBox.expand(infoLabel);
Component content = getContent(order); (2)
mainLayout.add(headerBox);
mainLayout.add(content);
mainLayout.expand(content);
return mainLayout;
}
1 | – 参考 DataGridDetailsGeneratorSample 类中的 createCloseButton 方法描述。 |
2 | – 参考 DataGridDetailsGeneratorSample 类中的 getContent 方法描述。 |
结果如图所示:
使用数据网格行内编辑器:
DataGrid
组件支持行内编辑器来编辑单元格数据。当用户要编辑一个数据项时,行内编辑界面会显示并自带默认的保存和取消按钮。
行内编辑器对应的方法有:
-
getEditedItemId()
- 返回正在被编辑的数据项的id
。 -
isEditorActive()
- 是否正在行内编辑界面编辑某个数据项。
-
editItem(Object itemId)
(废弃) - 为提供了id
的数据项打开编辑界面。如果数据项在当前界面区域不可见,数据网格会将数据项滚动到可视区域。
-
edit(Entity item)
- 为指定的数据项打开编辑界面。如果数据项在当前界面区域不可见,数据网格会将数据项滚动到可视区域。
使用以下方法添加/删除行内编辑界面打的监听器:
-
addEditorOpenListener()
,removeEditorCloseListener()
- 添加/删除行内编辑界面打开监听器。当用户双击
DataGrid
数据网格中某个区域时,行内编辑界面打开,使用上述监听器,可以获取被编辑行的其它字段并进行需要的修改。这种方法可以使得不用关闭当前行内编辑器就能修改其它字段。例如:
customersTable.addEditorOpenListener(editorOpenEvent -> { Map<String, Field> fieldMap = editorOpenEvent.getFields(); Field active = fieldMap.get("active"); Field grade = fieldMap.get("grade"); ValueChangeListener listener = e -> active.setValue(true); grade.addValueChangeListener(listener); });
-
addEditorCloseListener()
,removeEditorCloseListener()
- 添加/删除行内编辑界面关闭监听器。
-
addEditorPreCommitListener()
,removeEditorPreCommitListener()
- 添加/删除行内编辑界面数据提交前监听器。
-
addEditorPostCommitListener()
,removeEditorPostCommitListener()
- 添加/删除行内编辑界面数据提交后监听器。
行内编辑器所做的数据修改只提交到数据源或者数据容器。需要额外的代码把他们持久化到数据库。
可以通过 EditorFieldGenerationContext
类对编辑器组件进行定制,在某一列上使用 setEditFieldGenerator()
方法设置该列数据的编辑器组件:
@Inject
private DataGrid<Order> ordersDataGrid;
@Inject
private UiComponents uiComponents;
@Subscribe
protected void onInit(InitEvent event) {
ordersDataGrid.getColumnNN("amount").setEditFieldGenerator(orderEditorFieldGenerationContext -> {
LookupField<BigDecimal> lookupField = uiComponents.create(LookupField.NAME);
lookupField.setValueSource((ValueSource<BigDecimal>) orderEditorFieldGenerationContext
.getValueSourceProvider().getValueSource("amount"));
lookupField.setOptionsList(Arrays.asList(BigDecimal.ZERO, BigDecimal.ONE, BigDecimal.TEN));
return lookupField;
});
}
结果如下:
使用 ColumnGenerator(列生成器) 接口:
DataGrid
组件通过以下方法生成列:
-
addGeneratedColumn(String columnId, ColumnGenerator generator)
-
addGeneratedColumn(String columnId, ColumnGenerator generator, int index)
ColumnGenerator
是用来定义生成的列或者计算出的列的接口:
-
该列每一行的数据值,
-
该列数据类型。
下面是一个生成列的示例,这个列显示大写的用户登录名:
@Subscribe
protected void onInit(InitEvent event) {
DataGrid.Column column = usersGrid.addGeneratedColumn("loginUpperCase", new DataGrid.ColumnGenerator<User, String>(){
@Override
public String getValue(DataGrid.ColumnGeneratorEvent<User> event){
return event.getItem().getLogin().toUpperCase();
}
@Override
public Class<String> getType(){
return String.class;
}
}, 1);
column.setCaption("Login Upper Case");
}
结果如下:
ColumnGeneratorEvent
通过 getValue
方法传入, 它包含实体信息,在当前行的 ID 为 loginUpperCase
的列显示需要的数据。
默认情况下,新生成的列加在数据网格的最右边。有两种方法可以管理列的位置:代码中使用 index 或者在界面 XML 文件中提前定义好并设置 id
, 然后在 addGeneratedColumn
方法中使用该 id。
使用渲染器:
数据在列中的显示方式可以通过渲染器自定义。比如想在单元格中显示图标,可以使用 ImageRenderer
类和图标路径实现:
@Subscribe
protected void onInit(InitEvent event) {
DataGrid.Column avatar = usersGrid.addGeneratedColumn("userAvatar", new DataGrid.ColumnGenerator<User, String>() {
@Override
public String getValue(DataGrid.ColumnGeneratorEvent<User> event) {
return "icons/user.png";
}
@Override
public Class<String> getType() {
return String.class;
}
}, 0);
avatar.setCaption("Avatar");
avatar.setRenderer(usersGrid.createRenderer(DataGrid.ImageRenderer.class));
}
结果如下:
WebComponentRenderer
接口可以使得在数据网格单元格中显示不同的 Web 控件。以下是生成一个带查找控件的列的例子:
@Inject
private DataGrid<User> usersGrid;
@Inject
private UiComponents uiComponents;
@Inject
private Configuration configuration;
@Inject
private Messages messages;
@Subscribe
protected void onInit(InitEvent event) {
Map<String, Locale> locales = configuration.getConfig(GlobalConfig.class).getAvailableLocales();
Map<String, String> options = new TreeMap<>();
for (Map.Entry<String, Locale> entry : locales.entrySet()) {
options.put(entry.getKey(), messages.getTools().localeToString(entry.getValue()));
}
DataGrid.Column column = usersGrid.addGeneratedColumn("language",
new DataGrid.ColumnGenerator<User, Component>() {
@Override
public Component getValue(DataGrid.ColumnGeneratorEvent<User> event) {
LookupField<String> component = uiComponents.create(LookupField.NAME);
component.setOptionsMap(options);
component.setWidth("100%");
User user = event.getItem();
component.setValue(user.getLanguage());
component.addValueChangeListener(e -> user.setLanguage(e.getValue()));
return component;
}
@Override
public Class<Component> getType() {
return Component.class;
}
});
column.setRenderer(new WebComponentRenderer());
}
结果如下:
当字段类型与渲染器支持的类型不匹配时,可以创建一个 Function 来匹配模型和视图的数据类型。比如,想把布尔类型用图标展示时,可以巧妙的使用 HtmlRenderer
来做 HTML 渲染以及实现布尔类型转换为图标展示的逻辑。
@Inject
private DataGrid<User> usersGrid;
@Subscribe
protected void onInit(InitEvent event) {
DataGrid.Column<User> hasEmail = usersGrid.addGeneratedColumn("hasEmail", new DataGrid.ColumnGenerator<User, Boolean>() {
@Override
public Boolean getValue(DataGrid.ColumnGeneratorEvent<User> event) {
return StringUtils.isNotEmpty(event.getItem().getEmail());
}
@Override
public Class<Boolean> getType() {
return Boolean.class;
}
});
hasEmail.setCaption("Has Email");
hasEmail.setRenderer(
usersGrid.createRenderer(DataGrid.HtmlRenderer.class),
(Function<Boolean, String>) hasEmailValue -> {
return BooleanUtils.isTrue(hasEmailValue)
? FontAwesome.CHECK_SQUARE_O.getHtml()
: FontAwesome.SQUARE_O.getHtml();
});
}
结果如下:
渲染器可以通过两种方式创建:
-
在
DataGrid
接口的 set 方法中直接设置渲染器接口。 -
为对应的模块直接创建渲染器实现:
dataGrid.createRenderer(DataGrid.ImageRenderer.class) → new WebImageRenderer()
目前平台支持以下渲染器接口:
-
TextRenderer
- 显示文本。 -
HtmlRenderer
- 显示 HTML 布局。 -
ProgressBarRenderer
- 把 0 到 1 之间的double
浮点值作为ProgressBar
进度条组件显示。 -
DateRenderer
- 以预定义格式显示日期。 -
NumberRenderer
- 以预定义格式显示数字。 -
ButtonRenderer
- 把字符串值做为按钮文字展示。 -
ImageRenderer
- 将指定路径的图像显示。 -
CheckBoxRenderer
- 将布尔值做为复选框显示。
表头(Header)和表尾(Footer):
HeaderRow
和 FooterRow
接口受制于分别展示表头和表尾单元格,支持跨列合并单元格。
DataGrid
的以下方法用于创建和管理表头、表尾:
-
appendHeaderRow()
、appendFooterRow()
- 在表头或表尾区底部添加一个新行。 -
prependHeaderRow()
、prependFooterRow()
- 在表头或表尾区顶部添加一个新行。 -
addHeaderRowAt()
、addFooterRowAt()
- 在表头或表尾区的指定位置添加新行。该位置及其后面的行位置顺序下移,行索引增加。 -
removeHeaderRow()
、removeFooterRow()
- 从表头或表尾区删除指定行。 -
getHeaderRowCount()
、getFooterRowCount()
- 获取表头或表尾区行数。 -
setDefaultHeaderRow()
- 设置表头默认行。默认表头行为用户提供排序功能。
HeaderCell
和 FooterCell
接口提供自定义静态单元格功能:
-
setStyleName()
- 为单元格设置自定义样式。 -
getCellType()
- 返回单元格内容类型。静态单元格枚举类型DataGridStaticCellType
有三个值:-
TEXT
- 文本 -
HTML
- HTML -
COMPONENT
- 组件
-
-
getComponent()
、getHtml()
、getText()
- 不同类型单元格获取内容的方法。
下面这个例子中,表头包含合并的单元格,表尾显示经计算得出的值:
<dataGrid id="dataGrid" datasource="countryGrowthDs" width="100%">
<columns>
<column property="country"/>
<column property="year2017"/>
<column property="year2018"/>
</columns>
</dataGrid>
@Inject
private DataGrid<CountryGrowth> dataGrid;
@Inject
private UserSessionSource userSessionSource;
@Inject
private Messages messages;
@Inject
private CollectionContainer<CountryGrowth> countryGrowthsDc;
private DecimalFormat percentFormat;
@Subscribe
protected void onBeforeShow(BeforeShowEvent event) {
initPercentFormat();
initHeader();
initFooter();
initRenderers();
}
private DecimalFormat initPercentFormat() {
percentFormat = (DecimalFormat) NumberFormat.getPercentInstance(userSessionSource.getLocale());
percentFormat.setMultiplier(1);
percentFormat.setMaximumFractionDigits(2);
return percentFormat;
}
private void initRenderers() {
dataGrid.getColumnNN("year2017").setRenderer(new WebNumberRenderer(percentFormat));
dataGrid.getColumnNN("year2018").setRenderer(new WebNumberRenderer(percentFormat));
}
private void initHeader() {
DataGrid.HeaderRow headerRow = dataGrid.prependHeaderRow();
DataGrid.HeaderCell headerCell = headerRow.join("year2017", "year2018");
headerCell.setText("GDP growth");
headerCell.setStyleName("center-bold");
}
private void initFooter() {
DataGrid.FooterRow footerRow = dataGrid.appendFooterRow();
footerRow.getCell("country").setHtml("<strong>" + messages.getMainMessage("average") + "</strong>");
footerRow.getCell("year2017").setText(percentFormat.format(getAverage("year2017")));
footerRow.getCell("year2018").setText(percentFormat.format(getAverage("year2018")));
}
private double getAverage(String propertyId) {
double average = 0.0;
List<CountryGrowth> items = countryGrowthsDc.getItems();
for (CountryGrowth countryGrowth : items) {
Double value = countryGrowth.getValue(propertyId);
average += value != null ? value : 0.0;
}
return average / items.size();
}
- DataGrid 的属性列表
-
align - caption - captionAsHtml - colspan - columnResizeMode - columnsCollapsingAllowed - contextHelpText - contextHelpTextHtmlEnabled - contextMenuEnabled - css - dataContainer - datasource - description - descriptionAsHtml - editorBuffered - editorCancelCaption - editorEnabled - editorSaveCaption - enable - box.expandRatio - frozenColumnCount - headerVisible - height - icon - id - reorderingAllowed - responsive - rowspan - selectionMode - settingsEnabled - sortable - stylename - tabIndex - textSelectionEnabled - visible - width
- DataGrid 的元素
-
actions - buttonsPanel - columns - rowsCount
- column 元素的属性列表
-
caption - collapsed - collapsible - collapsingToggleCaption - editable - expandRatio - id - maximumWidth - minimumWidth - property - resizable - sortable - width
- column 的元素
- API
-
addGeneratedColumn - applySettings - createRenderer - edit - saveSettings - getColumns - setDescriptionProvider - setCellStyleProvider - setConverter - setDetailsGenerator - setEnterPressAction - setItemClickAction - setRenderer - setRowDescriptionProvider - setRowStyleProvider - sort
- DataGrid 监听器
-
ColumnCollapsingChangeListener - ColumnReorderListener - ColumnResizeListener - ContextClickListener - EditorCloseListener - EditorOpenListener - EditorPostCommitListener - EditorPreCommitListener - ItemClickListener - SelectionListener - SortListener
3.5.2.1.12. 日期时间组件
DateField
由日期控件和时间控件组成。日期控件是支持输入的控件,在输入框里面带有一个可以下拉选择日期的按钮,时间控件则在日期输入控件的右边:
该组件对应的 XML 名称:dateField
-
如需创建一个关联数据的日期控件,需要用 dataContainer 和 property 属性来设置:
<data> <instance id="orderDc" class="com.company.sales.entity.Order" view="_local"> <loader/> </instance> </data> <layout> <dateField dataContainer="orderDc" property="date"/> </layout>
在上面这个例子中,界面有
Order
实体的数据容器orderDc
,Order
实体拥有date
属性。XML 里面将dateField
的 dataContainer 属性指向这个数据容器,然后将 property 属性指向实体中需要显示在这个控件的字段。 -
如果这个控件关联实体的一个属性,它能根据实体属性的类型自动填充日期时间格式:
-
如果该控件不与实体属性相关联(比如没有设置数据容器和属性名称),可以使用
datatype
属性设置数据类型。DateField
使用如下数据类型:-
date
-
dateTime
-
localDate
-
localDateTime
-
offsetDateTime
-
-
日期时间格式也可以通过组件的
dateFormat
属性来设置。这个属性的值可以是一个定义日期时间格式的字符串或者语言包中的一个键。日期时间格式是使用
SimpleDateFormat
类提供的规则来定义。( http://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html )。 如果格式中没有H
或者h
的话,时间控件部分将不显示。<dateField dateFormat="MM/yy" caption="msg://monthOnlyDateField"/>
DateField
主要目的是通过填充占位符来使用键盘快速输入。所以这个组件只支持包含数字和分隔符的日期时间格式。那些复杂的含有星期文字表述或者月文字表述的格式目前是不支持的。
-
可以通过
rangeStart
和rangeEnd
属性来定义可选的日期范围。一旦日期范围设定了,其它在范围之外的日期都会变成不可选状态。日期范围可以用"yyyy-MM-dd"这样的格式在界面 XML 里面配置或者在程序里通过相应的 setter 来设置。<dateField id="dateField" rangeStart="2016-08-15" rangeEnd="2016-08-19"/>
-
DateField
组件值的变化,跟其它实现了Field
接口的组件一样,都可以用ValueChangeListener
来监听。可以使用isUserOriginated() 方法跟踪ValueChangeEvent
的来源。
-
日期和时间的精度可以用组件的
resolution
属性来定义,这个属性的值需要是DateField.Resolution
枚举类型 -SEC
、MIN
、HOUR
、DAY
、MONTH
、YEAR
。默认精度是MIN
,精确到分钟。如果
resolution="DAY"
而且dateFormat
没有给定的话,控件的显示格式会从主本地化消息包里的dateFormat
键获取。如果
resolution="MIN"
而且dateFormat
没有给定的话,控件的显示格式会从主本地化消息包里的dateTimeFormat
键获取。下面这个例子是精确到月的日期时间组件的写法:<dateField resolution="MONTH" caption="msg://monthOnlyDateField"/>
-
在选用 Halo-based 主题的 Web Client,如果需要无边框无背景的样式,可以通过预定义的
borderless
样式来实现。同样,也支持 XML 配置或者在界面控制器使用编程方法实现。<dateField id="dateField" stylename="borderless"/>
当用编程方法实现的时候,选用
HaloTheme
类的以DATEFIELD_
开头的常量:dateField.setStyleName(HaloTheme.DATEFIELD_BORDERLESS);
- dateField 的属性
-
align - caption - captionAsHtml - contextHelpText - contextHelpTextHtmlEnabled - css - dataContainer - datasource - datatype - dateFormat - description - descriptionAsHtml - editable - enable - box.expandRatio - height - icon - id - property - stylename - required - rangeEnd - rangeStart - requiredMessage - resolution - tabIndex - visible - width
- dateField 的 XML 元素
- dateField 的预定义样式
-
borderless - small - tiny
- API
-
addValueChangeListener - commit - discard - isModified - setContextHelpIconClickHandler
3.5.2.1.13. 日期选择器
DatePicker
是用来显示和选择日期的控件。跟DateField里面的下拉式日期选择器是有一样的外观。
该组件对应的 XML 名称: datePicker
-
可以使用dataContainer或datasource以及property属性来创建一个关联数据源的日期选择器:
<data> <instance id="orderDc" class="com.company.sales.entity.Order" view="_local"> <loader/> </instance> </data> <layout> <datePicker id="datePicker" dataContainer="orderDc" property="date"/> </layout>
在上面这个例子中,界面有
Order
实体的数据容器orderDc
,Order
实体拥有date
属性。XML 里面将datePicker
的 dataContainer 属性指向这个数据容器,然后将 property 属性指向实体中需要显示在这个控件的字段。
-
可以通过
rangeStart
和rangeEnd
属性来定义可选的日期范围。一旦日期范围设定了,其它在范围之外的日期都会变成不可选状态。<datePicker id="datePicker" rangeStart="2016-08-15" rangeEnd="2016-08-19"/>
-
日期和时间的精度可以用组件的
resolution
属性来定义,这个属性的值需要是DatePicker.Resolution
枚举类型 -DAY
、MONTH
、YEAR
。默认精度是DAY
。<datePicker id="datePicker" resolution="MONTH"/>
<datePicker id="datePicker" resolution="YEAR"/>
-
日历中的当前日期是根据用户浏览器的时间戳确定的,依赖操作系统的时区设置。用户会话的时区不会影响此功能。
- datePicker 的属性
-
align - caption - captionAsHtml - contextHelpText - contextHelpTextHtmlEnabled - css - dataContainer - datasource - datatype - description - descriptionAsHtml - editable - enable - box.expandRatio - height - id - property - rangeEnd - rangeStart - resolution - stylename - tabIndex - visible - width
- API
3.5.2.1.14. 嵌入式组件(废弃)
从 CUBA Platform 6.8 开始这个 |
Embedded
组件是用来显示图片和在应用程序界面中嵌入任何形式的网页。
该组件对应的 XML 名称: embedded
下面这个例子演示了怎样用这个组件显示一个从 FileStorage 读取的图片:
-
在 XML 描述中定义该组件:
<groupBox caption="Embedded" spacing="true" height="250px" width="250px" expand="embedded"> <embedded id="embedded" width="100%" align="MIDDLE_CENTER"/> </groupBox>
-
在界面控制器中,注入该组件和
FileStorageService
接口。在init()
方法中,从调用方获取FileDescriptor
,然后加载对应的文件到字节数组中,并通过字节数组创建ByteArrayInputStream
然后传给组件的setSource()
方法:@Inject private Embedded embedded; @Inject private FileStorageService fileStorageService; @Override public void init(Map<String, Object> params) { FileDescriptor imageFile = (FileDescriptor) params.get("imageFile"); byte[] bytes = null; if (imageFile != null) { try { bytes = fileStorageService.loadFile(imageFile); } catch (FileStorageException e) { showNotification("Unable to load image file", NotificationType.HUMANIZED); } } if (bytes != null) { embedded.setSource(imageFile.getName(), new ByteArrayInputStream(bytes)); embedded.setType(Embedded.Type.IMAGE); } else { embedded.setVisible(false); } }
Embedded
组件支持几种不同类型的内容,在 HTML 里以不同方式渲染。可以用 setType()
来设置内容类型。支持的类型如下:
-
OBJECT
- 允许在 HTML 的 <object> 和 <embed> 标签里嵌入特定的文件类型。 -
IMAGE
- 在 HTML 的 <img> 标签嵌入图片。 -
BROWSER
- 在 HTML 的 <iframe> 中嵌入一个网页。
在 Web Client 里面,这个组件支持显示存储在 VAADIN
文件夹的文件。可以采用相对路径来访问这个文件夹的资源,比如:
<embedded id="embedded"
relativeSrc="VAADIN/themes/halo/my-logo.png"/>
或者:
embedded.setRelativeSource("VAADIN/themes/halo/my-logo.png")
也可以通过应用程序属性 cuba.web.resourcesRoot 来定义源文件目录。然后采用 file://
、url://
,或者 theme://
这些前缀引用这个目录下的文件:
<embedded id="embedded"
src="file://my-logo.png"/>
或者
embedded.setSource("theme://branding/app-icon-menu.png");
如果要显示外部网页,把外部网页的 URL 传给这个组件就行了:
try {
embedded.setSource(new URL("http://www.cuba-platform.com"));
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
- embedded 的属性
-
align - caption - captionAsHtml - contextHelpText - contextHelpTextHtmlEnabled - description - descriptionAsHtml - height - id - relativeSrc - src - stylename - visible - width
3.5.2.1.15. 字段组
FieldGroup
用来集中显示和编辑实体的多个属性。
该组件对应的 XML 名称: fieldGroup
下面这个例子展示了在 XML 中定义一组字段的情况:
<dsContext>
<datasource id="orderDs"
class="com.sample.sales.entity.Order"
view="order-with-customer">
</datasource>
</dsContext>
<layout>
<fieldGroup id="orderFieldGroup" datasource="orderDs" width="250px">
<field property="date"/>
<field property="customer"/>
<field property="amount"/>
</fieldGroup>
</layout>
在上面这个例子中,dsContext
定义了一个包含单个实体 Order
的数据源 orderDs
。这里用 fieldGroup
组件的 datasource
属性来定义这个数据源。XML 元素 field
定义了那些需要显示在界面上的实体属性。
fieldGroup
的 XML 元素:
-
column
– 可选元素,用来把字段放到多个列显示。为了达到这样的效果,field
元素不能直接放在fieldGroup
元素里面,而需要放在一个column
元素里,比如:<fieldGroup id="orderFieldGroup" datasource="orderDs" width="100%"> <column width="250px"> <field property="num"/> <field property="date"/> <field property="amount"/> </column> <column width="400px"> <field property="customer"/> <field property="info"/> </column> </fieldGroup>
这样的话,字段会被排成两列;第一列包含的几个字段的宽度会是
250px
,第二列几个字段的宽度会是400px
。column
元素的属性:-
width
– 定义列中的字段宽度。默认的字段宽度是200px
。这里可以采用像素或者整个列宽的百分比来定义。
-
flex
– 伸缩率,定义当fieldGroup
整体的宽度发生变化时,此列相对于其它列水平伸缩的程度。比如,可以定义一列的flex=1
,另一列的flex=3
。
-
id
– 列 id,可选,在做界面扩展的时候会用到。
-
-
field
– 主要的组件元素,定义组件的一个字段。自定义的字段也可以放在
field
元素里:<fieldGroup> <field id="demo"> <lookupField id="demoField" datasource="userDs" property="group"/> </field> </fieldGroup>
field
元素的 XML 属性:-
id
– 如果property
没设置,那么必须设置 id;如果property
设置了,那么 id 默认跟property
取一样的值。id
属性需要使用唯一的标识符,要么是property
定义的字段名,要么是通过编程的方式定义的一个字段。如果是采取编程方式定义,那么field
也需要有custom="true"
(参阅下面 custom 的说明)
-
property
- 如果id
没设置,那么必须设置此属性;这个属性的值必须是一个实体属性的名称,用来显示这个绑定的字段。
-
visible
− 通过这个属性控制是否显示这个字段及其名称(caption)。
-
datasource
− 可以设置该字段单独的数据源,而不用整个fieldGroup
组件的数据源。这样的话,一个fieldGroup
就可以显示来自不同实体的属性了。 -
optionsDatasource
定义了一个用来做选项列表的数据源名称。可以给实体属性关联的字段定义选项数据源。默认情况下,选择关联实体的时候是通过一个查找界面来操作。但是如果optionsDatasource
属性设置了,则可以通过下拉列表来选择。也就是说,其实设置这个属性会导致原本默认的 LookupPickerField 会被 PickerField 替换掉。
-
width
− 设置字段宽度,不包括显示名称。默认是200px
。宽度值可以是像素值或者整个列宽的百分比。需要同时设置一列中所有的字段统一宽度,可以通过设置上面提到过的column
的width
属性。
-
custom
– 如果设置成true
,表示这个字段不关联实体的属性,也不关联一个对应的组件。然后这个字段需要通过FieldGroup
的setComponent()
方法以编程方式实现,具体可以参考下面对于setComponent()
的解释。
-
generator
属性用来以声明的方式创建自定义字段,需要设置这个属性的值为一个可以返回自定义组件的方法名:<fieldGroup datasource="productDs"> <column width="250px"> <field property="description" generator="generateDescriptionField"/> </column> </fieldGroup>
public Component generateDescriptionField(Datasource datasource, String fieldId) { TextArea textArea = uiComponents.create(TextArea.NAME); textArea.setRows(5); textArea.setDatasource(datasource, fieldId); return textArea; }
-
link
- 如果设置成true
,那么这个字段会显示成一个到实体编辑器的链接而不显示一个实体选择器字段。这个场景在只让用户看到关联的实体而不允许用户去修改实体关系的时候会用到。
-
linkScreen
- 当link
属性设置成true
,用来定义点击链接时需要打开的界面的标识符。
-
linkScreenOpenType
- 定义界面打开的类型(THIS_TAB
、NEW_TAB
或者DIALOG
)。
-
linkInvoke
- 定义一个控制器方法,在点击链接时调用这个方法,而不是打开界面。
根据需要显示的实体属性类型的不同,可以使用下面这些
field
的属性:-
mask
如果给一个文本型的实体属性设置这个mask
属性,那么界面组件会用 MaskedField 替换 TextField,并使用适当的掩码,此时也可以设置valueMode
属性。
-
rows
如果给一个文本型的实体属性设置这个rows
属性,那么界面组件会用 TextArea 替换 TextField,并将文字重组成适当的行数,此时也可以设置cols
属性。 -
maxLength
对于文本型实体属性,可以定义maxLength
属性,跟 TextField 中描述的一致。 -
dateFormat
对于date
或者dateTime
类型的实体属性,可以设置dateFormat
和resolution
参数,使用的组件是 DateField。 -
showSeconds
如果实体属性是time
类型,可以设置界面组件 TimeField 的showSeconds
属性。
-
fieldGroup
的 XML 属性:
-
border
属性可以设置成hidden
或者visible
。默认值是hidden
。如果设置成visible
,fieldGroup
组件会有边框(border)而且会被高亮。在 web 的实现中,通过添加cuba-fieldgroup-border
这个 CSS 类显示边框。
-
fieldFactoryBean
在 XML 描述里面,声明式的字段默认是通过FieldGroupFieldFactory
接口创建的。可以使用这个属性,覆盖默认工厂,将此属性设置成自定义FieldGroupFieldFactory
实现的名称。通过编程的方式创建
FieldGroup
的话,可以使用setFieldFactory()
方法。
FieldGroup
接口的方法:
-
addField
在运行时将字段添加到FieldGroup
。接受FieldConfig
类型实例作为参数,也可以通过colIndex
和rowIndex
参数定义字段的位置。
-
bind()
在setDatasource()
之后触发的方法,用来为添加的字段绑定相应的 UI 组件。
-
createField()
用来创建新的实现了FieldConfig
接口的 FieldGroup 元素:fieldGroup.addField(fieldGroup.createField("newField"));
-
getComponent()
返回一个跟字段绑定的可视化组件。这个也许在需要设置额外的组件参数时,通过这个方法得到这个可视化组件。因为上面提到的field
提供的 XML 配置的参数有限。在界面控制器中,如果想获取对可视化组件的引用,可以采用注入的方式,而不是通过显式地调用
getFieldNN("id").getComponentNN()
的方式。具体做法是,使用@Named
注解,提供的注解参数是fieldGroup
的标识符加.
再加上字段标识符。比如下面例子中,在用一个字段选择关联的实体的时候,可以添加一个 Open 操作,然后删掉这个字段的 Clear 操作:
<fieldGroup id="orderFieldGroup" datasource="orderDs"> <field property="date"/> <field property="customer"/> <field property="amount"/> </fieldGroup>
@Named("orderFieldGroup.customer") protected PickerField customerField; @Override public void init(Map<String, Object> params) { customerField.addOpenAction(); customerField.removeAction(customerField.getAction(PickerField.ClearAction.NAME)); }
要使用
getComponent()
获取或者注入字段组件,需要知道字段中使用的组件的类型。下面这个表列举了实体的属性类型和组件的对应关系:实体属性类型 附加条件 字段可视化组件类型 关联实体
指定了
optionsDatasource
枚举类型 (
enum
)string
指定了
mask
指定了
rows
boolean
date
,dateTime
time
int
,long
,double
,decimal
指定了
mask
UUID
MaskedField 16 进制掩码
-
removeField()
支持在运行时根据id
移除字段.
-
setComponent()
为字段设置自定义的可视化组件。可以在 XML 元素field
的属性custom="true"
或者使用createField()
方法创建字段时使用。当与custom="true"
一起使用的时候,数据源(datasource)和对应的属性(property)需要手动设置。FieldConfig
接口类的实例可以通过getField()
或者getFieldNN()
方法获取,然后就可以调用它的setComponent()
方法:@Inject protected FieldGroup fieldGroup; @Inject protected UiComponents uiComponents; @Inject private Datasource<User> userDs; @Override public void init(Map<String, Object> params) { PasswordField passwordField = uiComponents.create(PasswordField.NAME); passwordField.setDatasource(userDs, "password"); fieldGroup.getFieldNN("password").setComponent(passwordField); }
- fieldGroup 的属性
-
align - border - caption - captionAsHtml - captionAlignment - contextHelpText - contextHelpTextHtmlEnabled - css - datasource - description - descriptionAsHtml - editable - enable - box.expandRatio - fieldFactoryBean - id - height - stylename - visible - width
- column 的属性
- field 的属性
-
caption - captionProperty - cols - custom - datasource - dateFormat - description - editable - enable - generator - id - link - linkInvoke - linkScreen - linkScreenOpenType - mask - maxLength - optionsDatasource - property - required - requiredMessage - resolution - rows - showSeconds - tabIndex - visible - width
- field 的 XML 元素
-
column - field - inputPrompt - validator
- API
-
addField - bind - createField - getComponent - removeField - setComponent - setFieldFactory
3.5.2.1.16. 多文件上传控件
FileMultiUploadField
组件允许用户把文件上传到服务器。这个组件是个按钮;用户点击时,系统自带的文件选择器会弹出,此时用户可以选择多个文件来上传。
该组件对应的 XML 名称: multiUpload
下面是一个使用 FileMultiUploadField
的示例。
-
在界面的 XML 描述中声明这个组件:
<multiUpload id="multiUploadField" caption="Upload Many"/>
-
在界面控制器中,需要注入该组件本身,还需要注入 FileUploadingAPI 和 DataManager 这两个接口。
@Inject private FileMultiUploadField multiUploadField; @Inject private FileUploadingAPI fileUploadingAPI; @Inject private Notifications notifications; @Inject private DataManager dataManager; @Subscribe protected void onInit(InitEvent event) { (1) multiUploadField.addQueueUploadCompleteListener(queueUploadCompleteEvent -> { (2) for (Map.Entry<UUID, String> entry : multiUploadField.getUploadsMap().entrySet()) { (3) UUID fileId = entry.getKey(); String fileName = entry.getValue(); FileDescriptor fd = fileUploadingAPI.getFileDescriptor(fileId, fileName); (4) try { fileUploadingAPI.putFileIntoStorage(fileId, fd); (5) } catch (FileStorageException e) { throw new RuntimeException("Error saving file to FileStorage", e); } dataManager.commit(fd); (6) } notifications.create() .withCaption("Uploaded files: " + multiUploadField.getUploadsMap().values()) .show(); multiUploadField.clearUploads(); (7) }); multiUploadField.addFileUploadErrorListener(queueFileUploadErrorEvent -> { notifications.create() .withCaption("File upload error") .show(); }); }
1 在 onInit()
方法里面,添加了事件监听器,这样可以在文件上传成功或者出错时做出反馈。2 该组件将所有选择的文件上传到客户端层(client tier) 的临时存储(temporary storage)并且调用通过 addQueueUploadCompleteListener()
方法添加的监听器。3 在这个监听器里面,会调用 FileMultiUploadField.getUploadsMap()
方法获得临时存储的文件标识和文件名映射关系的 map。4 然后,通过调用 FileUploadingAPI.getFileDescriptor()
为每一条 map 记录创建相应的FileDescriptor
对象。com.haulmont.cuba.core.entity.FileDescriptor
(别跟java.io.FileDescriptor
混淆了) 是一个持久化实体,唯一定义一个上传的文件,并且也用这个类从系统下载文件。5 FileUploadingAPI.putFileIntoStorage()
方法用来把文件从客户端层的临时存储移动到 FileStorage。这个方法的参数是临时存储中文件的标识符和对应的FileDescriptor
对象。6 在将文件上传到 FileStorage
之后,通过调用DataManager.commit()
方法将FileDescriptor
实例存到数据库。这个方法的返回值可以用来设置给一个实体的属性,这个属性关联此文件。这里,FileDescriptor
简单的保存在数据库。上传的文件可以通过 Administration > External Files 界面查看。7 完成整个上传过程之后,文件列表需要通过调用 clearUploads()
方法清空以便下一次上传再使用。
下面列出能跟踪上传进度的监听器:
-
FileUploadErrorListener
-
FileUploadStartListener
-
FileUploadFinishListener
-
QueueUploadCompleteListener
最大可上传的文件大小是由 cuba.maxUploadSizeMb 应用程序属性定义的,默认是 20MB。如果用户选择了更大的文件的话,会有相应的提示信息,并且中断上传过程。
multiUpload
属性:
-
accept
XML 属性 (或者相应的setAccept()
方法) 用来设置文件选择对话框里面的文件类型掩码,但是用户还是可以选择“所有文件”来上传任意文件。这个属性的值需要是以英文逗号分隔的文件后缀名,比如:
*.jpg,*.png
。
-
fileSizeLimit
XML 属性 (或者相应的setFileSizeLimit()
方法) 用来设置最大允许上传的文件大小。这个设置是针对每一个文件都有效的。<multiUpload id="multiUploadField" fileSizeLimit="200000"/>
-
permittedExtensions
XML 属性 (或者相应的setPermittedExtensions()
方法) 设置允许的文件扩展名白名单。这个属性的值需要是字符串的集合,其中每个字符串是以
.
开头的允许的文件扩展名,比如:uploadField.setPermittedExtensions(Sets.newHashSet(".png", ".jpg"));
-
dropZone
XML 属性允许设置一个特殊的 BoxLayout 用来作为从浏览器外部拖拽文件可以放置的目标容器区域。如果这个容器的样式没有特殊设置,当文件被拖拽到这块区域的时候,这个容器会被高亮显示,否则目标区域不会显示。
参考 加载和显示图片 有更多复杂的使用上传文件的例子。
- multiUpload 的属性
-
accept - align - caption - captionAsHtml - css - description - descriptionAsHtml - dropZone - enable - fileSizeLimit - height - box.expandRatio - icon - id - pasteZone - permittedExtensions - stylename - tabIndex - visible - width
- multiUpload 监听器
-
FileUploadErrorListener - FileUploadFinishListener - FileUploadStartListener - QueueUploadCompleteListener
3.5.2.1.17. 文件上传控件
FileUploadField
允许用户上传文件到服务器。这个控件包含标题 、 已上传文件的链接 、 还有两个按钮:上传按钮和清除文件选择按钮。当点击上传按钮的时候,会弹出系统标准的文件选择器,用户可以在这里选择需要上传的文件。如果是要上传多个文件,可以用 FileMultiUploadField 控件。
该控件对应的 XML 名称:upload
对于 FileDescriptor
类型的实体属性,可以在 FieldGroup 内用 datasource 属性使用此控件,也可以在 Form 中通过 dataContainer 属性使用,或者也能单独使用。如果此控件绑定到任何数据组件,上传的文件会被立即保存到文件存储,对应的 FileDescriptor
实例会保存到数据库。
<upload fileStoragePutMode="IMMEDIATE"
dataContainer="personDc"
property="photo"/>
还可以通过编程的方式控制文件和 FileDescriptor
的保存:
-
在界面的 XML 描述中声明这个控件:
<upload id="uploadField" fileStoragePutMode="MANUAL"/>
-
在界面控制器中,需要注入该控件本身,还需要注入 FileUploadingAPI 和 DataSupplier 这两个接口。订阅
InitEvent
事件并且添加事件监听器,这样可以对文件上传成功或者出错做出相应的操作:@Inject private FileUploadField uploadField; @Inject private FileUploadingAPI fileUploadingAPI; @Inject private DataSupplier dataSupplier; @Inject private Notifications notifications; @Subscribe protected void onInit(InitEvent event) { uploadField.addFileUploadSucceedListener(uploadSucceedEvent -> { File file = fileUploadingAPI.getFile(uploadField.getFileId()); (1) if (file != null) { notifications.create() .withCaption("File is uploaded to temporary storage at " + file.getAbsolutePath()) .show(); } FileDescriptor fd = uploadField.getFileDescriptor(); (2) try { fileUploadingAPI.putFileIntoStorage(uploadField.getFileId(), fd); (3) } catch (FileStorageException e) { throw new RuntimeException("Error saving file to FileStorage", e); } dataSupplier.commit(fd); (4) notifications.create() .withCaption("Uploaded file: " + uploadField.getFileName()) .show(); }); uploadField.addFileUploadErrorListener(uploadErrorEvent -> notifications.create() .withCaption("File upload error") .show()); }
1 | 此时如果需要的话,可以取到保存在临时存储的文件。 |
2 | 一般来说,文件需要保存到中间件的文件存储中。 |
3 | 将文件保存至 FileStorage。 |
4 | 将文件描述器保存到数据库。 |
该控件将所有选择的文件上传到客户端层(client tier) 的临时存储(temporary storage)并且调用通过 addFileUploadSucceedListener()
添加的监听器。在这个监听器里面,从 uploadField 获取了一个 FileDescriptor
。
com.haulmont.cuba.core.entity.FileDescriptor
(别跟 java.io.FileDescriptor
混淆了) 是一个持久化实体,唯一定义一个上传的文件,并且也用这个类从系统下载文件。
FileUploadingAPI.putFileIntoStorage()
方法把文件从客户端层的临时存储移动到文件存储。这个方法的参数是临时存储中文件的标识符和对应的 FileDescriptor
对象,这两个参数都是由 FileUploadField
提供的。
当上传文件到 FileStorage
完成后,通过调用 DataSupplier.commit()
把 FileDescriptor
实例存到数据库。这个方法会返回保存的实体,可以用来赋值给其它实体里关联这个文件的属性。这里只是简单的把 FileDescriptor
存到了数据库。从 Administration > External Files 界面可以看到这个文件。
通过 addFileUploadErrorListener()
方法添加的监听器会在从客户端上传文件到临时存储出错的时候被调用。
下面列出能跟踪上传进度的监听器:
-
AfterValueClearListener
-
BeforeValueClearListener
-
FileUploadErrorListener
-
FileUploadFinishListener
-
FileUploadStartListener
-
FileUploadSucceedListener
-
ValueChangeListener
fileUploadField
的属性:
-
fileStoragePutMode
- 定义文件和相应的FileDescriptor
怎么存储。-
IMMEDIATE
模式,在文件存到客户端层临时存储之后立即存储。 -
MANUAL
模式, 需要手动在FileUploadSucceedListener
里面编码实现。当在
FieldGroup
里面使用FileUploadField
的时候,默认模式是IMMEDIATE
,其它情况下,默认模式是MANUAL
。
-
-
uploadButtonCaption
、uploadButtonIcon
和uploadButtonDescription
这三个 XML 属性可以设置上传按钮的属性。
-
showFileName
- 控制上传文件的名称是否要显示在上传按钮旁边,默认是false
不显示。
-
showClearButton
- 控制是否要显示清空按钮,默认false
不显示。
-
clearButtonCaption
、clearButtonIcon
和clearButtonDescription
这三个 XML 属性可以设置清空按钮的属性。
-
accept
XML 属性 (或者相应的setAccept()
方法) 用来设置文件选择对话框里面的文件类型掩码,但是用户还是可以选择“所有文件”来上传任意文件。这个属性的值需要是以英文逗号分隔的文件扩展名,比如:
*.jpg,*.png
。 -
最大可上传的文件大小是由 cuba.maxUploadSizeMb 应用程序属性定义的,默认是 20MB。如果用户选择了更大的文件的话,会有相应的提示信息,并且中断上传过程。
-
fileSizeLimit
XML 属性 (或者相应的setFileSizeLimit()
方法) 用来设置最大允许上传的文件大小,以字节为单位。<upload id="uploadField" fileSizeLimit="2000"/>
-
permittedExtensions
XML 属性 (或者相应的setPermittedExtensions()
方法) 设置允许的文件扩展名白名单。这个属性的值需要是字符串的集合,其中每个字符串是以
.
开头的允许的文件扩展名,比如:uploadField.setPermittedExtensions(Sets.newHashSet(".png", ".jpg"));
-
dropZone
- 允许设置一个特殊的 BoxLayout 用来作为从浏览器外部拖拽文件可以放置的目标容器区域。这个目标区域可以覆盖整个对话框的窗口。当文件被拖拽到这块区域的时候,这个容器会被高亮显示,否则目标区域不会显示。<layout spacing="true" width="100%"> <vbox id="dropZone" height="AUTO" spacing="true"> <textField id="textField" caption="Title" width="100%"/> <textArea id="textArea" caption="Description" width="100%" rows="5"/> <checkBox caption="Is reference document" width="100%"/> <upload id="upload" dropZone="dropZone" showClearButton="true" showFileName="true"/> </vbox> <hbox spacing="true"> <button caption="mainMsg://actions.Apply"/> <button caption="mainMsg://actions.Cancel"/> </hbox> </layout>
如果想要 dropZone 不变并且一直显示,需要给这个容器设置预定义的样式名称
dropzone-container
。此时这个容器应该是空的,只包含一个label
组件:<layout spacing="true" width="100%"> <textField id="textField" caption="Title" width="100%"/> <checkBox caption="Is reference document" width="100%"/> <upload id="upload" dropZone="dropZone" showClearButton="true" showFileName="true"/> <vbox id="dropZone" height="150px" spacing="true" stylename="dropzone-container"> <label stylename="dropzone-description" value="Drop file here" align="MIDDLE_CENTER"/> </vbox> <hbox spacing="true"> <button caption="mainMsg://actions.Apply"/> <button caption="mainMsg://actions.Cancel"/> </hbox> </layout>
-
pasteZone
允许设置一个特殊的容器用来处理粘贴(paste)的快捷键。此时需要这个容器内部的一个文字输入控件获得焦点(focused)。这个功能只支持基于 Chromium 的浏览器。<upload id="uploadField" pasteZone="vboxId" showClearButton="true" showFileName="true"/>
参考 加载和显示图片 有更多复杂的使用上传文件的例子。
- upload 的属性
-
accept - align - caption - captionAsHtml - clearButtonCaption - clearButtonDescription - clearButtonIcon - contextHelpText - contextHelpTextHtmlEnabled - css - dataContainer - datasource - description - descriptionAsHtml - dropZone - editable - enable - box.expandRatio - fileSizeLimit - fileStoragePutMode - height - icon - id - pasteZone - permittedExtensions - property - showClearButton - showFileName - stylename - tabIndex - uploadButtonCaption - uploadButtonDescription - uploadButtonIcon - visible - width
- API
-
addValueChangeListener - commit - discard - isModified - setContextHelpIconClickHandler
- upload 监听器
-
AfterValueClearListener - BeforeValueClearListener - FileUploadErrorListener - FileUploadFinishListener - FileUploadStartListener - FileUploadSucceedListener
3.5.2.1.18. 过滤器
在这章节包含下面这些内容:
Filter
是一个具有非常多功能的过滤器,可以对展示成列表或者表格形式的数据库实体列表进行过滤。这个组件支持按照任意条件对数据进行快速过滤,同时也支持创建可重复使用的过滤器。
Filter
需要连接到一个包含数据加载器的集合型数据容器或者包含 JPQL查询的集合型数据源。实现的主要逻辑是按照用户设置的过滤条件对这个 JPQL 查询进行修改。所以,过滤其实是发生在数据库层面,通过执行修改后的 SQL,查询出来的数据被加载到中间件和客户端层。
使用过滤器
一个典型的过滤器是这样的:
默认情况下,这个组件使用快速过滤模式。意味着用户可以添加一组过滤条件进行一次数据搜索,一旦这个界面关掉之后,设置的过滤条件也就没了。
创建一个快速过滤器,点击 Add search condition - 添加搜索条件 链接,会显示条件选择的界面:
下面是一些可用的过滤条件类型:
-
Attributes - 属性 – 实体属性和关联的实体,只能用持久化的实体属性。这些属性要满足下面两种情况之一:要么在过滤器的 XML 描述里面显式的设置在
property
元素里,要么符合 properties 元素定义的规则。 -
Custom conditions - 自定义条件 – 由开发人员在过滤器 XML 描述中的
custom
元素设置的过滤条件。 -
Create new… - 新建过滤器… – 创建新的 JPQL 条件过滤器。这个选项只对具有
cuba.gui.filter.customConditions
权限的用户开放。
选中的过滤条件会在过滤器区域顶部显示。这个 条件移除图标会在每个条件的旁边显示,允许移除已选择的条件。
可以保存快速过滤器以便将来使用。要保存一个快速过滤器,点击过滤器设置按钮,选择 Save/Save as - 保存/另存为 然后在对话框输入一个新的过滤器名字:
保存之后,这个过滤器就会在 Search - 搜索 按钮的下拉框中显示。
Reset filter 菜单可以用来重置当前应用的查询条件。
用于过滤器设置的弹窗按钮提供一系列过滤器管理的选项:
-
Save - 保存 – 保存当前过滤器的修改
-
Save with values - 带值保存 – 保存当前过滤器的修改,并且将参数编辑器里面的值保存为过滤器的默认条件值。
-
Save as - 另存为 – 将过滤器另存为一个新名称。
-
Edit - 编辑 – 打开过滤器编辑(参阅下面)。
-
Make default - 设置默认 – 设置当前界面的默认过滤器。当界面打开时,这个过滤器会自动显示在过滤器区域。
-
Remove - 删除 – 删除当前的过滤器。
-
Pin applied - 保留已选 – 使用上次查询的结果来做级联过滤(参考级联过滤 )。
-
Save as search folder - 另存为搜索文件夹 – 以当前的过滤器创建一个文件夹。
-
Save as application folder - 另存为应用程序文件夹 – 以当前的过滤器创建一个应用程序文件夹。此功能只对有
cuba.gui.appFolder.global
权限的用户开放。
Edit 选项打开过滤器编辑器,可以对当前过滤器进行高级设置:
Name 字段应该填写过滤器的名称。这个名称会显示在当前界面可用的过滤器列表里。
过滤器可以通过 Available to all users 复选框设置成 全局 的(也就是所有用户都能用),或者通过 Global default 复选框设置成 全局默认 的。这些操作需要一个特殊的权限,叫做 CUBA > Filter > Create/modify global filters。如果这个过滤器被标记成 全局默认 的话,那么当任何用户打开这个界面的时候,就会自动加载这个过滤器的数据。用户可以使用 Default for me 复选框设置他们自己的默认过滤器,这个设置会覆盖 全局默认 过滤器。
这些过滤器的过滤条件包含在树状结构里,可以通过 Add 按钮添加,通过 交换位置,或者通过 Remove 按钮删除。
AND 或者 OR 分组条件可以通过相应的按钮添加,所有顶层过滤条件(比如没有显式的分组)都是通过 AND 连接。
在树状结构选择过滤条件时,会在编辑器的右边打开一个条件属性的列表。
过滤条件可以通过相应的复选框设置成隐藏或者必要。隐藏的条件参数对用户来说是不可见的,所以应该在编辑过滤器的时候显示出来。
Width 属性是在过滤器区域为当前条件的字段设置显示宽度。默认情况下,在过滤器区域的条件都显示成三列。这里字段的显示宽度也就是字段需要占据的列的数目(1,2 或者 3)。
当前条件的默认值可以在 Default value 里面选择。
自定义的过滤器条件名称可以设置在 Caption 字段。
Operation 提供选择条件的运算符。跟据属性的类型确定可选的运算符列表。
如果实体有 DateTime
类型的属性,且此属性没有 @IgnoreUserTimeZone 注解,那么在过滤器里面会采用用户的时区默认作为这个属性的时区。如果是 Date
类型的话,可以通过自定义过滤条件编辑器里面的 Use time zone 标记来定义是否使用用户的时区来处理这个字段。
过滤器组件介绍
该组件对应的 XML 名称: filter
下面是在界面 XML 中定义这个组件的示例:
<data readOnly="true">
<collection id="carsDc" class="com.haulmont.sample.core.entity.Car" view="carBrowse">
<loader id="carsDl" maxResults="50">
<query>
<![CDATA[select e from sample_Car e order by e.createTs]]>
</query>
</loader>
</collection>
</data>
<layout expand="carsTable" spacing="true">
<filter id="filter" applyTo="carsTable" dataLoader="carsDl">
<properties include=".*"/>
</filter>
<table id="carsTable" width="100%" dataContainer="carsDc">
<columns>
<column id="vin"/>
<column id="colour"/>
<column id="model"/>
</columns>
<rowsCount/>
</table>
</layout>
在上面的例子中,在界面的 data
层定义了一个数据容器使用 JPQL 查询 Car
实体。在 filter
组件的 loader
属性中定义了需要被过滤的数据,数据由 数据加载器 提供。采用表格组件显示数据,也是关联了相同的数据容器。
filter
可以包含嵌套元素。这些元素主要用来描述用户可以在 Add Condition 对话框中能使用的过滤条件:
-
properties
– 多个实体属性通过这项配置成可用。这个元素有如下属性:-
include
– 必带属性。包含一个正则表达式,能匹配实体的属性名称。
-
exclude
– 包含一个正则表达式,如果实体属性能匹配此项,那么会从之前的include
配置中排除掉。
-
excludeProperties
– 包含一个英文逗号分隔的应该被排除掉的属性路径列表。跟之前的exclude
不同,这里支持遍历实体关系图,比如customer.name
。
-
excludeRecursively
- 设置excludeProperties
里面定义的属性是否需要递归的排除掉。如果设置的true
,那么属性和它的嵌套属性,只要是相同名称的,都会被排除掉。示例:
<filter id="filter" applyTo="ordersTable" dataLoader="ordersDl"> <properties include=".*" exclude="(amount)|(id)" excludeProperties="version,createTs,createdBy,updateTs,updatedBy,deleteTs,deletedBy" excludeRecursively="true"/> </filter>
通过编程的方式排除属性,使用
Filter
组件的setPropertiesFilterPredicate()
方法:filter.setPropertiesFilterPredicate(metaPropertyPath -> !metaPropertyPath.getMetaProperty().getName().equals("createTs"));
当使用
properties
元素的时候,下面这些实体属性不能作为过滤条件: -
-
property
– 显式地根据属性名来包含一个实体属性。这个元素有下面这些属性:-
name
– 必须属性,指定需要包含的实体属性的名称。可以是实体关系图里面的路径(使用“.”)比如:<filter id="transactionsFilter" dataLoader="transactionsDl" applyTo="table"> <properties include=".*" exclude="(masterTransaction)|(authCode)"/> <property name="creditCard.maskedPan" caption="msg://EmbeddedCreditCard.maskedPan"/> <property name="creditCard.startDate" caption="msg://EmbeddedCreditCard.startDate"/> </filter>
-
paramWhere
− 在参数是关联的实体的情况下,用这个参数来设置 JPQL 表达式用以选取条件参数的列表。这里需要用{E}
占位符来代表实体而不能用实体的别名。比如,假设
Car
有Model
的引用,那么参数值的列表可以限制到只取Audi
型号:<filter id="carsFilter" dataLoader="carsDl"> <property name="model" paramWhere="{E}.manufacturer = 'Audi'"/> </filter>
使用会话(session)和界面参数的例子如下:
{E}.createdBy = :session$userLogin and {E}.name like :param$groupName
使用
paramWhere
语句,可以引入参数之间的依赖。比如,假设Manufacturer
是一个独立的实体。Car
有Model
的属性,而Model
又有Manufacturer
的属性。那么可以给 Cars 创建两个过滤条件:第一个选择一个 Manufacturer,第二个选择 Model。为了用前一个过滤条件选出的 manufacturer 来限制第二个过滤条件 models 的列表,可以在paramWhere
表达式添加一个参数:{E}.manufacturer.id = :component$filter.model_manufacturer90062
这个参数引用了一个显示 Manufacturer 参数的组件。如果在过滤器编辑界面鼠标右键点击过滤条件列表的一行,可以在弹出菜单中看到组件的名称:
-
paramView
− 指定一个视图。如果过滤器参数关联了一个实体,可以用视图来加载过滤条件参数值列表。比如,_local
。如果视图没有指定,默认会使用_minimal
视图。
-
-
custom
这个元素用来定义一个定制化的过滤条件。元素的内容需要是 JPQL 表达式(也能使用JPQL 宏),这个表达式会被添加到数据容器查询语句的where
条件后面。这里需要用{E}
占位符来代表实体而不能用实体的别名。这个条件最多只能用一个以“?”标记的参数。定制化的条件的值可以用特殊字符,比如"like"操作符需要的 "%" 或者 "_"。如果需要转义这些字符,可以在条件里面添加
escape '<char>'
,比如:{E}.name like ? escape '\'
这样如果采用
foo\%
作为过滤条件的参数值,搜索时会将"%"作为普通字符而非特殊字符。下面这个例子演示了采用定制化条件的过滤器:
<filter id="carsFilter" dataLoader="carsDl"> <properties include=".*"/> <custom name="vin" paramClass="java.lang.String" caption="msg://vin"> {E}.vin like ? </custom> <custom name="colour" paramClass="com.company.sample.entity.Colour" caption="msg://colour" inExpr="true"> ({E}.colour.id in (?)) </custom> <custom name="repair" paramClass="java.lang.String" caption="msg://repair" join="join {E}.repairs cr"> cr.description like ? </custom> <custom name="updateTs" caption="msg://updateTs"> @between({E}.updateTs, now-1, now+1, day) </custom> </filter>
custom
过滤条件在 Add condition 窗口的 Custom conditions 区域显示:custom
的 XML 属性:-
name
− 必须,过滤条件的名称。
-
paramClass
− 过滤条件参数的 Java 类。如果参数没有指定,这个参数就不必须。
-
inExpr
− 如果 JPQL 表达式中包含in (?)
,则这个属性就需要设置成true
。这样的话,用户能手动输入几个过滤条件参数值。
-
join
− 可选属性。设置一个字符串,会被添加在数据容器查询语句的from
部分。如果要依赖关联实体集合的属性来创建一个复杂的过滤条件,可以用这个属性。这个属性值应该包含join
和left join
语句。比如,假设
Car
实体有repairs
属性,关联Repair
实体的集合。那么可以创建下面这个过滤条件来使用Repair
实体的description
属性过滤Car
:<filter id="carsFilter" dataLoader="carsDl"> <custom name="repair" caption="msg://repair" paramClass="java.lang.String" join="join {E}.repairs cr"> cr.description like ? </custom> </filter>
如果用了上面这个过滤条件,原来的数据容器查询语句
select c from sample_Car c order by c.createTs
会被转化成下面这样:
select c from sample_Car c join c.repairs cr where (cr.description like ?) order by c.createTs
-
paramWhere
− 在参数是关联的实体的情况下,用这个参数来设置 JPQL 表达式用以选取条件参数的列表。参考property
元素的同名属性的描述。 -
paramView
− 指定一个 视图。如果过滤器参数关联了一个实体,可以用视图来加载过滤条件参数值列表。参考property
元素的同名属性的描述。
-
filter
属性:
-
editable
– 如果这个属性是false
,Edit 选项会被禁用。
-
manualApplyRequired
− 定义过滤器生效的时机。如果这个属性设置的false
,这个过滤器(默认或者空)会在界面打开时生效。意味着数据容器会被刷新、与其关联的界面组件(比如表格)会显示数据。如果这个属性设置为true
,只有当用户点击 Search - 搜索 按钮的时候才会生效。这个属性的优先级比应用程序属性 cuba.gui.genericFilterManualApplyRequired 的优先级要高。
-
useMaxResults
− 是否需要限制加载到数据容器的数据量。默认设置的true
。如果这个属性设置的
false
,过滤器不会显示 Show rows - 显示行数 控件。数据容器中的数据记录条数(同时也显示在表格里)只受到实体统计的MaxFetchUI
参数限制,默认是 10000 条。如果这个属性没设置或者设置为
true
,只有当用户有cuba.gui.filter.maxResults
权限的情况下才显示 Show rows 控件。如果用户没有获得cuba.gui.filter.maxResults
权限的授权,过滤器会强制加载 N 条数据,用户也没办法去禁用或者指定另一个 N 值。N 是用FetchUI
和DefaultFetchUI
参数定义的,这两个参数是从实体统计机制中读取的。下面这个过滤器有这些参数:
useMaxResults="true"
、cuba.gui.filter.maxResults
权限被禁止 、DefaultFetchUI = 2
。
-
textMaxResults
- 使用文本输入控件而不是下拉列表来作为 Show rows 控件。默认值false
。
-
folderActionsEnabled
− 如果设置成false
,这两个操作会被隐藏:Save as Search Folder 、 Save as Application Folder。默认值是true
,这些操作都可用。
-
applyTo
− 可选属性,包含跟过滤器关联的组件标识符。当需要访问关联组件的presentations - 展示设置的时候需要设置这个属性。比如,保存过滤器作为搜索文件夹或者应用程序文件夹的时候,当要访问这些目录的时候就会应用展示样式。
-
columnsCount
- 定义过滤器区域过滤条件所占的列数。默认是 3。
-
defaultMode
- 定义过滤器的默认模式。可以选generic
和fts
。如果设置的fts
,过滤器会用全文检索的模式打开(实体需要建立全文检索的索引)。默认值是generic
。
-
modeSwitchVisible
- 定义是否显示切换到全文检索模式的复选框。如果全文检索不可用,不管这个值如何设定,复选框都不可见。默认是true
。
Filter 接口的方法
-
setBorderVisible()
- 设置是否需要显示过滤器的边框,默认是true
。
Filter 的监听器
-
ExpandedStateChangeListener
- 过滤器面板展开状态改变监听器。
-
FilterEntityChangeListener
- 当组件初始化的时候第一次选择过滤器或者之后切换到其它保存的过滤器的时候触发。
- filter 的属性
-
applyTo - caption - captionAsHtml - columnsCount - css - dataLoader - datasource - defaultMode - description - descriptionAsHtml - editable - enable - box.expandRatio - folderActionsEnabled - id - manualApplyRequired - margin - modeSwitchVisible - settingsEnabled - stylename - textMaxResults - useMaxResults - visible - width
- filter 的 XML 元素
-
custom - properties - property
- properties 的 XML 属性
- property 的 XML 属性
-
caption - name - paramView - paramWhere
- custom 的 XML 属性
-
caption - name - inExpr - join - paramClass - paramView - paramWhere
- API
-
addExpandedStateChangeListener - addFilterEntityChangeListener - applySettings - getMargin - saveSettings - setMargin
用户权限
-
需要创建/修改/删除全局(对所有用户可见)过滤器,用户需要有
cuba.gui.filter.global
权限。 -
需要创建/修改
custom
过滤条件,用户需要有cuba.gui.filter.customConditions
权限。 -
需要使用 Show rows 控件修改表格每页加载的最大行数,用户需要有
cuba.gui.filter.maxResults
权限。参考过滤器属性 useMaxResults。
其它特定的权限配置内容,参考 安全子系统。
外部控制过滤器的参数
-
系统级参数
下面这些应用程序属性会影响过滤器的行为:
-
cuba.gui.genericFilterManualApplyRequired − 禁用界面打开时过滤器自动生效(比如加载数据)还可以参考 manualApplyRequired 过滤器属性。
-
cuba.gui.genericFilterChecking − 在应用过滤器前,检查至少配置了一个过滤条件。
-
cuba.gui.genericFilterControlsLayout − 定义过滤器控件的内部布局。
-
cuba.gui.genericFilterColumnsCount - 设置过滤器区域摆放过滤条件的默认列数。也可参考 columnsCount 过滤器属性。
-
cuba.gui.genericFilterConditionsLocation - 定义过滤器区域的位置。
-
cuba.gui.genericFilterPopupListSize - 定义 Search 按钮弹出框能显示的最多条目数。
-
cuba.gui.genericFilterTrimParamValues - 定义所有通用过滤器是否要先清掉输入文本两端的空格。
-
-
界面调用参数
在界面打开时,系统应当提供自动生效的过滤器及其默认参数。为了实现这个效果,这个过滤器需要提前创建,保存在数据库,
SEC_FILTER
表需要有一条相应的记录,并且CODE
字段需要有值。界面调用的参数都在web-menu.xml
配置文件里面设置。要把过滤器保存在数据库,过滤器的 insert 脚本应当添加在实体的
30.create-db.sql
数据库脚本里。为了简化脚本的创建,可以在 Administration - 管理 菜单的 Entity Inspector - 实体探查 子菜单找到过滤器实体,右键点击过滤器列表选择 System Information - 系统信息,点击 Script for insert - 插入脚本 按钮,可以拷贝脚本内容。然后可以修改界面默认使用这个过滤器。要指定过滤器的代码,需要传给界面一个跟过滤器组件同名的参数,参数的值是过滤器的代码。
需要设置过滤器参数值的话,需要传给界面参数的名称跟过滤器的参数名称一致,然后这些参数的值是过滤器的值,需要是字符串类型。
下面是一个在描述文件中定义主菜单项的例子。这里用
FilterByVIN
代码为sample$Car.browse
界面carsFilter
组件设置过滤器。同时还给component$carsFilter.vin79216
条件设置了参数值TMA
:<item id="sample$Car.browse"> <param name="carsFilter" value="FilterByVIN"/> <param name="component$carsFilter.vin79216" value="TMA"/> </item>
需要注意的是,定义了
CODE
字段的过滤器有些特性:-
不能被用户编辑。
-
过滤器的名称可以多语言显示。可以在主消息包里面给过滤器代码设置名称。
-
级联使用过滤器
如果设置了 cuba.allowQueryFromSelected 应用程序属性,可以用组件的界面上暂存(pin)上次和本次过滤器应用的结果。保存之后,另一个过滤器或者当前过滤器采用不同参数就可以在目前过滤出的数据范围内进行进一步的过滤。
这个方案可以达到以下两个目标:
按照下面的步骤使用级联过滤。首先,选择并且应用一个过滤器。然后点击过滤器设置按钮选择 Pin applied。然后这个过滤器会被固定到过滤区域的顶部。之后另一个过滤器可以在此基础上应用,如此往复。级联过滤器数量没有限制。也可以通过 按钮移除之前固定的过滤器。
能连续使用过滤器是基于 DataManager的级联查询功能。
过滤器参数的 API
Filter
接口提供了在界面控制器读写过滤器参数的方法:
-
setParamValue(String paramName, Object value)
-
getParamValue(String paramName)
paramName
- 过滤器参数名称。参数名称是显示参数值的组件的一部分。获取组件名称的过程上面说过了。参数名称放置在组件名称的最后一个 .
的后面。比如,如果组件名称是 component$filter.model_manufacturer90062
,那么参数名称是 model_manufacturer90062
。
过滤器的全文搜索模式
如果过滤器的数据容器被全文检索子系统(参考 CUBA Platform. 全文搜索)做了全文索引的话,那么这个过滤器可以使用全文检索模式。用 Full-Text Search 复选框切换到这个模式。
在全文检索模式里,过滤器包含了文本控件用来做搜索规则,搜索也是在 FTS 子系统做了索引的那些实体字段中进行。
如果在 applyTo 属性定义了一个表格,当把鼠标指针移到表格的某一行的时候,就会提示哪些实体属性支持查询条件。
如果要隐藏过滤器模式切换的复选框,可以设置 modeSwitchVisible 为 false
。
如果需要过滤器默认就打开全文检索模式,可以设置 defaultMode 为 fts
。
全文检索跟其他任意过滤器条件组合使用:
FTS condition 可以在条件选择器窗口进行选择。
3.5.2.1.19. 表单
Form
被用于多个实体属性的联合显示和编辑。它是一个类似于GridLayout的简单容器,可以有一定数量的嵌套列,嵌套字段的类型在 XML 中以声明方式定义,字段的标题位于字段的左侧。与 GridLayout
的主要区别在于 Form
能够将所有嵌套字段绑定到一个数据容器中。
从平台版本 7.0 开始,生成的编辑界面默认使用 Form
代替FieldGroup。
该组件对应的 XML 名称:form
下面是在界面 XML 描述中定义一组字段的示例:
<data>
<instance id="orderDc"
class="com.company.sales.entity.Order"
view="order-edit">
<loader/>
</instance>
</data>
<layout expand="editActions" spacing="true">
<form id="form" dataContainer="orderDc">
<dateField property="date"/>
<textField property="amount"/>
<pickerField property="customer"/>
</form>
</layout>
在上面的例子中,data
上下文定义了一个 orderDc
数据容器,它包含 Order
实体的单个实例。数据容器在 form
组件的 dataContainer
属性中指定。嵌套元素指的是需要在组件中显示的实体属性。它们会根据属性数据类型自动类型化。
form
的属性:
-
childrenCaptionWidth
– 为所有嵌套列及其子元素指定固定标题宽度。设置-1
使用自动大小。
-
dataContainer
- 为嵌套字段设置数据容器。
form
的元素:
-
column
– 可选元素,允许将字段放置在多列。为此,嵌套字段不应该直接放在form
元素中,而应放在column
中。例如:<form id="form" dataContainer="orderDc"> <column width="250px"> <dateField property="date"/> <textField property="amount"/> </column> <column width="400px"> <pickerField property="customer"/> <textArea property="info"/> </column> </form>
在这种情况下,字段将排列成两列; 第一列所有字段的宽度为
250px
,第二列所有字段的宽度为400px
。column
的属性:-
id
– 一个可选的列标识符,允许在界面扩展时引用它。
-
width
– 指定列的字段宽度。默认情况下,字段的宽度为200px
。在此属性中,可以以像素为单位指定宽度,也可以以列的水平宽度的百分比指定宽度。
-
childrenCaptionWidth
– 为嵌套字段指定固定的标题宽度。设置-1
使用自动大小。
-
Form
接口的方法:
-
add()
- 允许在运行时向Form
添加一个或多个字段。它接受一个Component
实例作为参数,也可以通过添加column
和row
索引来定义新字段的位置。框架不会为使用编程方式添加的组件指定数据容器,在这种情况下,使用
setValueSource()
方法进行数据绑定。示例:
<data> <instance id="container"/> </data> <layout> <form id="form"/> </layout>
@Inject private KeyValueContainer container; @Inject private Form form; @Subscribe protected void onInit(InitEvent event) { Field fooField = (Field) uiComponentsGenerator.generate( new ComponentGenerationContext(container.getEntityMetaClass(), "foo")); fooField.setCaption("Foo"); fooField.setValueSource(new ContainerValueSource<>(container, "foo")); form.add(fooField); }
- form 的属性
-
align - box.expandRatio - caption - captionAsHtml - captionPosition - childrenCaptionWidth - colspan - contextHelpText - contextHelpTextHtmlEnabled - css - dataContainer - description - descriptionAsHtml - editable - enable - height - icon - id - responsive - rowspan - stylename - visible - width
- column 的属性
- API
3.5.2.1.20. 分组表格
GroupTable
分组表格能动态支持按任意字段把数据分组。如果需要基于某列分组,则把该列拖拽到 元素的左侧。被分组的数据可以通过 / 按钮展开/收起。
该组件对应的 XML 名称为: groupTable
。
必须为 GroupTable
分组表格定义 CollectionContainer
类型的数据容器或者groupDatasource 分组数据源。否则,分组功能不可用。示例:
<data>
<collection id="ordersDc"
class="com.company.sales.entity.Order"
view="order-with-customer">
<loader id="ordersDl">
<query>
<![CDATA[select e from sales_Order e]]>
</query>
</loader>
</collection>
</data>
<layout>
<groupTable id="ordersTable"
width="100%"
dataContainer="ordersDc">
<columns>
<group>
<column id="date"/>
</group>
<column id="customer"/>
<column id="amount"/>
</columns>
<rowsCount/>
</groupTable>
</layout>
group
是 columns 中的可选元素,它包含若干列 column
,打开对应界面时,会默认将数据按这些列进行分组。
可以针对每个列 column
设置 groupAllowed
布尔值属性,控制该列是否可以用来分组数据。
如果 aggregatable
属性设置为 true
, 则会针对每组显示聚合结果;并在第一行显示针对所有行的聚合结果。如果 showTotalAggregation
属性设置为 false
, 针对所有行的聚合结果则不会显示。
如果 multiselect
多选属性设置为 true
, 按下 Ctrl 键并单击分组行时,该组会展开,该组的所有行都会被选上。但反过来不同,如果整组都被选上,Ctrl+单击 并不会反选所有组数据。通过 Ctrl 还是可以反选特定的行。
-
GroupTable
分组表格接口的方法: -
-
groupByColumns()
- 基于给定列进行分组。以下示例中,会将数据先以 department 分组,再以 city 分组:
groupTable.groupByColumns("department", "city");
-
ungroupByColumns()
- 取消基于给定列的分组。以下示例中,会取消针对 department 的分组, 但基于 city 的分组会保留。
groupTable.ungroupByColumns("department");
-
ungroup()
- 取消所有分组。
-
GroupTable
分组表格的其它功能类似于普通表格.
- GroupTable 的属性列表
-
align - aggregatable - aggregationStyle - caption - captionAsHtml - columnControlVisible - contextHelpText - contextHelpTextHtmlEnabled - contextMenuEnabled - css - dataContainer - description - descriptionAsHtml - editable - enable - box.expandRatio - height - id - multiLineCells - multiselect - presentations - reorderingAllowed - settingsEnabled - showTotalAggregation - sortable - stylename - tabIndex - textSelectionEnabled - visible - width
- GroupTable 的元素
-
actions - buttonsPanel - columns - rows - rowsCount
- columns 的元素
- column 列属性列表
-
align - caption - captionProperty - collapsed - dateFormat - editable - groupAllowed - id - link - linkInvoke - linkScreen - linkScreenOpenType - maxTextLength - optionsDatasource - resolution - sortable - visible - width
- column 列的元素
- rows 行集合的属性
- API
-
addColumnCollapseListener - addSelectionListener - groupByColumns - setClickListener - ungroup - ungroupByColumns
3.5.2.1.21. 图片组件
Image
图片组件可以显示不同源的图片。可以绑定到数据容器或通过代码设置。
该组件的 XML 名称为: image
Image
图片组件可以显示实体属性为 FileDescriptor
或 byte[]
类型的数据。下面是一个简单的通过 dataContainer
和 property
属性设置图片的例子:
<image id="image" dataContainer="employeeDc" property="avatar"/>
该组件展示 employeeDc
数据容器中 Employee
实体的 avatar
属性。
Image
图片组件还可以展示其它源的图片。可通过以下 image
的元素设置不同的源类型:
-
classpath
- classpath 中的某个资源<image> <classpath path="com/company/sample/web/screens/myPic.jpg"/> </image>
-
file
- 文件系统中的某个资源<image> <file path="D:\sample\modules\web\web\VAADIN\images\myImage.jpg"/> </image>
-
relativePath
- 应用程序目录中的某个资源<image> <relativePath path="VAADIN/images/myImage.jpg"/> </image>
-
theme
- 主题资源,例如VAADIN/themes/customTheme/some/path/image.png
。<image> <theme path="com.company.sample/myPic.jpg"/> </image>
-
url
- 可以从指定 URL 加载的资源<image> <url url="https://www.cuba-platform.com/sites/all/themes/cuba_adaptive/img/lori.png"/> </image>
image
图片组件的属性:
-
scaleMode
- 缩放模式,有以下几种模式可选:-
FILL
- 根据组件大小拉伸图片。 -
CONTAIN
- 保留长宽比压缩图片到能刚好在组件中全部展示。 -
SCALE_DOWN
- 在NONE
和CONTAIN
中选择图片能全部展示并且尺寸最小的方式。 -
NONE
- 按实际大小显示。
-
-
alternateText
- 设置替换文本,当资源未设置或找不到时显示该文本。<image id="image" alternateText="logo"/>
image
资源设置:
-
bufferSize
- 下载该资源时的缓存大小,以字节为单位。<image> <file bufferSize="1024" path="C:/img.png"/> </image>
-
cacheTime
- 该资源缓存过期时间,以毫秒为单位。<image> <file cacheTime="2400" path="C:/img.png"/> </image>
-
mimeType
- 该资源的 MIME 类型。<image> <url url="https://avatars3.githubusercontent.com/u/17548514?v=4&s=200" mimeType="image/png"/> </image>
Image
图片组件接口的方法:
-
setDatasource()
- 设置图片数据源类型,只支持FileDescriptor
和byte[]
两种类型。数据源可以通过编辑的方式设置,比如在表单元格中显示图片:
frameworksTable.addGeneratedColumn("image", entity -> { Image image = uiComponents.create(Image.NAME); image.setDatasource(frameworksTable.getItemDatasource(entity), "image"); image.setHeight("100px"); return image; });
-
setSource()
- 设置图片源内容。输入源类型,返回源对象,并继续通过流式接口配置源内容。每种源类型都有各自设置源内容的方法,比如ThemeResource
主题源用setPath()
;StreamResource
流资源用setStreamSupplier()
:Image image = uiComponents.create(Image.NAME); image.setSource(ThemeResource.class) .setPath("images/image.png");
或:
image.setSource(StreamResource.class) .setStreamSupplier(() -> new FileDataProvider(fileDescriptor).provide()) .setBufferSize(1024);
使用以下实现了
Resource
接口的资源类型,或者通过扩展它实现自定义资源:-
ClasspathResource
- 位于 classpath 中的图片. 这类资源还可以通过image
组件的classpath
元素以声明的方式设置。 -
FileDescriptorResource
- 通过FileDescriptor
从FileStorage
中获取的图片。 -
FileResource
- 文件系统中的图片。这类资源还可以通过image
组件的file
元素以声明的方式设置。 -
RelativePathResource
- 应用程序中的图片。这类资源还可以通过image
组件的relativePath
元素以声明的方式设置。 -
StreamResource
- 来自于流的图片。 -
ThemeResource
- 主题的图片,比如VAADIN/themes/yourtheme/some/path/image.png
。这类资源还可以通过image
组件的theme
元素以声明的方式设置。 -
UrlResource
- 从 URL 中加载的图片。这类源还可以通过image
组件的url
元素以声明的方式设置。
-
-
createResource()
- 根据图片源类型创建图片资源。创建的对象可以传入setSource()
方法。FileDescriptorResource resource = image.createResource(FileDescriptorResource.class) .setFileDescriptor(avatar); image.setSource(resource);
-
addClickListener()
- 设置点击图片区域的监听器。image.addClickListener(clickEvent -> { if (clickEvent.isDoubleClick()) notifications.create() .withCaption("Double clicked") .show(); });
-
addSourceChangeListener()
- 设置图片源改变的监听器。
- Image 图片组件的属性列表
-
align - alternateText - caption - captionAsHtml - colspan - contextHelpText - contextHelpTextHtmlEnabled - css - dataContainer - datasource - description - descriptionAsHtml - editable - enable - box.expandRatio - height - icon - id - property - required - requiredMessage - responsive - rowspan - scaleMode - stylename - stylename - visible - width
- Image resources 图片源的属性列表
- Image 图片组件的元素
-
classpath - file - relativePath - theme - url
- API
-
addClickListener - addSourceChangeListener - createResource - setScaleMode - setDatasource - setSource
3.5.2.1.22. 标签组件
Label
组件可以展示静态文本或者实体属性值。
该组件的 XML 名称是: label
下面是使用从本地化消息包中获取文本来设置标签的例子:
<label value="msg://orders"/>
value
属性设置标签的文本值。
<label value="Label, which should be split into multiple lines"
width="200px"/>
可以在界面控制器中设置标签的参数,前提是给标签控件设置一个 id,然后在界面控制器中获取它的引用:
<label id="dynamicLabel"/>
@Inject
private Label dynamicLabel;
@Subscribe
protected void onInit(InitEvent event) {
dynamicLabel.setValue("Some value");
}
Label
组件还可以显示实体属性值。这种情况需要设置 dataContainer 和 property 属性,例如:
<data>
<instance id="customerDc" class="com.company.sales.entity.Customer" view="_local">
<loader/>
</instance>
</data>
<layout>
<label dataContainer="customerDc" property="name"/>
</layout>
上面例子里,标签组件显示 customerDс
数据容器中实体 Customer
的 name
字段。
htmlEnabled
属性控制如何解析 value 属性值:如果 htmlEnabled="true"
,则 value
值以 HTML 代码解析,否则按纯文本解析。
- 标签样式
-
在基于 Halo 主题的网页端, 可以通过
stylename
属性定义样式。在 XML 描述里或者在界面控制器中:<label value="Label to be styled" stylename="colored"/>
通过代码设置样式时,选择
HaloTheme
类中以LABEL_
开头的常量:label.setStyleName(HaloTheme.LABEL_COLORED);
-
bold
- 加粗。适用于重要的或者需要突出显示的文本。
-
colored
- 彩色文本。
-
failure
- 失败标签样式。标签外会有一个边框,文本旁边会有一个图标。适用于一些组件内部的上下文通知。
-
h1
- 标题样式,应用程序标题。
-
h2
- 标题样式,应用程序中章节标题。
-
h3
- 标题样式,应用程序子章节标题。
-
h4
- 标题样式,应用程序小章节标题。
-
light
- 纤细。适用于附加/补充文本。
-
no-margin
- 不要默认边距。
-
spinner
- 回旋样式。添加到空Label
组件则可以创建一个可用于表示任务进行中(比如数据正在加载中…)的旋转图标。
-
success
- 成功标签样式。标签外会有一个边框,文本旁会有一个图标。适用于一些组件内部的上下文通知。
-
- Label 组件的属性列表
-
align - css - dataContainer - datasource - description - descriptionAsHtml - enable - box.expandRatio - height - htmlEnabled - icon - id - property - stylename - value - visible - width
- Label 组件的元素
- Label 组件样式
-
bold - colored - failure - h1 - h2 - h3 - h4 - huge - large - light - no-margin - small - spinner - success - tiny
- API
3.5.2.1.23. 链接组件
Link
链接组件为一个超链接,可以打开外部 web 资源。
该组件的 XML 名称为: link
以下为一个 link
的 XML 描述示例:
<link caption="Link" url="https://www.cuba-platform.com" target="_blank" rel="noopener"/>
link
组件的属性:
-
url
– 对应 web 资源的 url 地址。
-
target
– 为 Web 客户端设置 web 页面的打开模式,与<a>
HTML 元素的target
属性相同。
-
rel
- 可选属性,指定当前文档与链接文档的关系。与<a>
HTML 元素中的rel
属性相同。默认值为
"noopener noreferrer"
。-
noopener
- 通过些链接打开的浏览器上下文中不能使用 opener,即window.opener
为null
。 -
noreferrer
- 不为后续链接提供 HTTP referer header。
-
- Link 组件的属性列表
-
align - caption - captionAsHtml - css - description - descriptionAsHtml - enable - box.expandRatio - icon - id - rel - stylename - url - target - visible - width
3.5.2.1.24. 链接按钮
LinkButton
组件外观类似超链接,本质是一个按钮。
该组件的 XML 名称是: linkButton
LinkButton
可以包含文本或图标(或二者均有)。下图展示了不同类型的按钮:
LinkButton 与普通 Button
的不同仅在于外观。所有的属性和行为都与 Button 中描述的一样。
以下是一个 LinkButton 的 XML 描述示例,它调用了控制器的 someMethod()
方法。还设置了caption属性,description属性(做为提示)和icon属性:
<linkButton id="linkButton"
caption="msg://linkButton"
description="Press me"
icon="SAVE"
invoke="someMethod"/>
- LinkButton 的属性列表
-
action - align - caption - captionAsHtml - css - description - descriptionAsHtml - enable - box.expandRatio - icon - id - invoke - stylename - visible - width
3.5.2.1.25. 下拉框
该控件支持从下拉列表中选择值。下拉列表提供基于用户的输入对数据进行过滤的功能,也支持分页显示数据。
该组件的 XML 名称是: lookupField
-
使用
LookupField
下拉框最简单的例子是从实体属性中选择枚举值。比如,Role
角色实体中有type
属性,为枚举类型。用户可以通过LookupField
下拉框控件编辑该属性:<data> <instance id="roleDc" class="com.haulmont.cuba.security.entity.Role" view="_local"> <loader/> </instance> </data> <layout expand="editActions" spacing="true"> <lookupField dataContainer="roleDc" property="type"/> </layout>
在上面的例子中,使用
roleDc
数据容器选择Role
实体数据。lookupField
下拉框组件中,数据容器的连接通过 dataContainer 属性定义,实体属性的名称定义在 property 属性中。此时,这个属性为枚举类型,控件的下拉列表中会显示所有枚举值的本地化名称。 -
类似的,
LookupField
下拉框也可以用来选择实体实例。选项容器属性可以用来创建一系列选项:<data> <instance id="carDc" class="com.haulmont.sample.core.entity.Car" view="carEdit"> <loader/> </instance> <collection id="colorsDc" class="com.haulmont.sample.core.entity.Color" view="_minimal"> <loader id="colorsDl"> <query> <![CDATA[select e from sample$Color e]]> </query> </loader> </collection> </data> <layout> <lookupField dataContainer="carDc" property="color" optionsContainer="colorsDc"/> </layout>
这种时候,
colorsDc
数据容器中的Color
实体的实例名称会显示在下拉列表中,被选择的值会被设置到carDc
数据容器中Car
实体的color
属性中。captionProperty定义显示到下拉框的实体属性值,而非实例名称,这样可以设置下拉列表的文字值。
-
选项列表还可以通过
setOptionsList()
,setOptionsMap()
和setOptionsEnum()
设置,也可以在 XML 描述中通过optionsContainer
或者optionsDatasource
属性设置。-
setOptionsList()
- 通过代码指定选项列表。首先在 XML 描述中声明组件:<lookupField id="numberOfSeatsField" dataContainer="modelDc" property="numberOfSeats"/>
然后将组件注入界面控制器,在
onInit()
方法中设置选项列表:@Inject protected LookupField<Integer> numberOfSeatsField; @Subscribe public void onInit(InitEvent event) { List<Integer> list = new ArrayList<>(); list.add(2); list.add(4); list.add(5); list.add(7); numberOfSeatsField.setOptionsList(list); }
组件的下拉列表中会显示 2, 4, 5 和 7。被选择的值会设置到
modelDc
数据容器中的numberOfSeats
属性上。
-
setOptionsMap()
可以提供选项 map。比如为 XML 描述中声明的numberOfSeatsField
组件指定选项 map(在onInit()
方法中):@Inject protected LookupField<Integer> numberOfSeatsField; @Subscribe public void onInit(InitEvent event) { Map<String, Integer> map = new LinkedHashMap<>(); map.put("two", 2); map.put("four", 4); map.put("five", 5); map.put("seven", 7); numberOfSeatsField.setOptionsMap(map); }
组件下拉框会显示
two
,four
,five
,seven
文本。但是组件的值则是与文本对应的数字值。数字值会被设置到modelDc
数据容器中的numberOfSeats
属性上。
-
setOptionsEnum()
需要枚举类做为参数。下拉列表会显示枚举值的本地化名称,组件的值则为枚举值。
-
-
通过
setOptionStyleProvider()
可以为组件显示的不同的选项设置分别的 style name:lookupField.setOptionStyleProvider(entity -> { User user = (User) entity; switch (user.getGroup().getName()) { case "Company": return "company"; case "Premium": return "premium"; default: return "company"; } });
-
下拉列表中的组件可以在左边设置对应的图标。在界面控制器中使用
setOptionIconProvider()
方法设置:lookupField.setOptionIconProvider(entity -> { if (entity.getType() == LegalStatus.LEGAL) return "icons/icon-office.png"; return "icons/icon-user.png"; });
如果使用 SVG 图标, 显式设置图标大小以避免图标覆盖。
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="enable-background:new 0 0 55 55;" viewBox="0 0 55 55" height="25px" width="25px">
-
如果
LookupField
下拉框组件非required,并且对应的实体属性也非必须,下拉选项会包含一个空行。如果选择了空行,组件值为null
. 使用 nullName 属性设置在“空行”上显示的文本。以下为一个示例:<lookupField dataContainer="carDc" property="colour" optionsContainer="colorsDs" nullName="(none)"/>
这样,下拉框中的“空行”上会显示
(none)
文本。如果用户选择了该行,对应的实体属性值会设置为null
。如果在代码中通过
setOptionsList()
设置了选项, 可以用setNullOption()
方法设置空行文本,这样,如果用户选择了该行,组件值则为null
。- LookupField 过滤器:
-
-
filterMode
属性设置基于用户输入的过滤模式:-
NO
− 不过滤。 -
STARTS_WITH
− 选项文本以用户输入开头。 -
CONTAINS
− 选项文本包含用户输入(默认模式)。
-
-
setFilterPredicate()
方法用来设置过滤方法,该方法判断元素是否跟查找文字匹配。比如:BiFunction<String, String, Boolean> predicate = String::contains; lookupField.setFilterPredicate((itemCaption, searchString) -> predicate.apply(itemCaption.toLowerCase(), searchString));
FilterPredicate
的test
方法可以用来客户化过滤逻辑,比如处理方言/特殊字符:lookupField.setFilterPredicate((itemCaption, searchString) -> StringUtils.replaceChars(itemCaption, "ÉÈËÏÎ", "EEEII") .toLowerCase() .contains(searchString));
-
-
LookupField
组件在没有合适选项的时候可以处理用户的输入,通过setNewOptionHandler()
来处理,示例:@Inject private Metadata metadata; @Inject private LookupField<Color> colorField; @Inject private CollectionContainer<Color> colorsDc; @Subscribe protected void onInit(InitEvent event) { colorField.setNewOptionHandler(caption -> { Color color = metadata.create(Color.class); color.setName(caption); colorsDc.getMutableItems() .add(color); colorField.setValue(color); }); }
当用户输入不匹配任何选项的值并且按下回车键时,会触发调用新选项处理器。这时,上述代码会创建一个新的
Color
实例,name
属性值为用户输入,并且这个实例会加到下拉选项数据容器中,做为该控件当前被选中值。除了使用
setNewOptionHandler()
方法来处理用户输入,还可以在 XML 描述中通过newOptionHandler
属性来指定控制器处理的方法。这个方法需要两个参数,一个是LookupField
类型,指向组件实例,另一个是String
类型,指向用户输入。newOptionAllowed
属性也可以用来开启是否允许输入新值。
-
nullOptionVisible
XML 属性设置是否在下拉列表显示空值。可以配置LookupField
下拉框非 required但是不提供空值选项。
-
textInputAllowed
属性可以禁止过滤器功能及键盘输入。对短列表来说很方便。默认值为true
。
-
pageLength
属性重新设置下拉列表中一页选项的个数,默认值在cuba.gui.lookupFieldPageLength应用程序属性中定义。 -
在基于 Halo 主题的 Web 客户端,可以自定义样式,通过 XML
stylename
属性或在界面控制器中通过代码设置:<lookupField id="lookupField" stylename="borderless"/>
通过代码设置时,从
HaloTheme
类中选择LOOKUPFIELD_
开头的常量样式:lookupField.setStyleName(HaloTheme.LOOKUPFIELD_BORDERLESS);
LookupField 下拉框样式有:
-
align-center
- 文本居中对齐。
-
align-right
- 文本靠右对齐。
-
borderless
- 文本不要边框和背景。
-
- LookupField 下拉框属性列表
-
align - caption - captionAsHtml - captionProperty - contextHelpText - contextHelpTextHtmlEnabled - css - dataContainer - datasource - description - descriptionAsHtml - editable - enable - box.expandRatio - filterMode - height - icon - id - inputPrompt - newOptionAllowed - newOptionHandler - nullName - nullOptionVisible - optionsDatasource - optionsEnum - pageLength - property - required - requiredMessage - stylename - tabIndex - textInputAllowed - visible - width
- LookupField 下拉框元素
- LookupField 下拉框样式
-
align-right - align-center - borderless - huge - large - small - tiny
- API
-
addValueChangeListener - commit - discard - isModified - setContextHelpIconClickHandler - setFilterPredicate - setOptionsEnum - setOptionsList - setOptionsMap - setOptionsStyleProvider
3.5.2.1.26. 下拉选择器
LookupPickerField
下拉选择器支持在文本框中显示实体实例,从下拉列表选择实例,点击右侧的按钮触发操作。
该组件的 XML 名称为: lookupPickerField
事实上,LookupPickerField
下拉选择器是LookupField和PickerField的组合。所以它与 LookupField 有相同的功能,但是默认的操作不一样。LookupPickerField
的默认操作是 lookup
和 open
。
下面是一个用 LookupPickerField
给 Car
实体的 color
属性提供选项值的例子:
<data>
<instance id="carDc" class="com.haulmont.sample.core.entity.Car" view="carEdit">
<loader/>
</instance>
<collection id="colorsDc" class="com.haulmont.sample.core.entity.Color" view="_minimal">
<loader id="colorsDl">
<query>
<![CDATA[select e from sample$Color e]]>
</query>
</loader>
</collection>
</data>
<layout>
<lookupPickerField dataContainer="carDc" property="color" optionsContainer="colorsDc"/>
</layout>
- LookupPickerField 下拉选择器的属性列表
-
align - caption - captionAsHtml - captionProperty - contextHelpText - contextHelpTextHtmlEnabled - css - dataContainer - datasource - description - descriptionAsHtml - editable - enable - box.expandRatio - filterMode - height - icon - id - inputPrompt - metaClass - newOptionAllowed - newOptionHandler - nullName - optionsDatasource - pageLength - property - required - requiredMessage - stylename - tabIndex - visible - width
- LookupPickerField 下拉选择器的元素
- LookupPickerField 下拉选择器的样式
- API
-
addValueChangeListener - commit - discard - isModified - setContextHelpIconClickHandler - setOptionsStyleProvider
3.5.2.1.27. 掩码字段
这是一个文本字段控件,其中的数据以预定义格式输入。例如,使用 MaskedField
输入电话号码很方便。
该组件对应的 XML 名称: maskedField
MaskedField
基本上复制了 TextField 的功能,但是不能为掩码字段设置 datatype
。因此,MaskedField
仅适用于 String
类型的文本和实体属性。MaskedField
具有以下特定属性:
-
mask
– 为字段设置掩码。要设置掩码,请使用以下字符:-
#
– 数字 -
U
– 大写字母 -
L
– 小写字母 -
?
– 字母 -
А
– 字母或数字 -
*
– 任何字符 -
H
– 大写十六进制字符 -
h
– 小写十六进制字符 -
~
– " +" 或者 "-" 字符
-
-
valueMode
– 定义返回值的格式(带掩码或不带掩码),可以使用masked
或clear
作为值。
下面提供了带有用于输入电话号码的掩码的文本字段示例:
<maskedField id="phoneNumberField" mask="(###)###-##-##" valueMode="masked"/>
<button id="showPhoneNumberBtn" caption="msg://showPhoneNumberBtn"/>
@Inject
private MaskedField phoneNumberField;
@Inject
private Notifications notifications;
@Subscribe("showPhoneNumberBtn")
protected void onShowPhoneNumberBtnClick(Button.ClickEvent event) {
notifications.create()
.withCaption((String) phoneNumberField.getValue())
.withType(Notifications.NotificationType.HUMANIZED)
.show();
}
- maskedField 的属性
-
align - caption - captionAsHtml - contextHelpText - contextHelpTextHtmlEnabled - css - dataContainer - datasource - description - descriptionAsHtml - editable - enable - box.expandRatio - height - icon - id - mask - maxLength - property - required - requiredMessage - stylename - tabIndex - trim - valueMode - visible - width
- maskedField 的元素
- API
-
addEnterPressListener - addValueChangeListener - setContextHelpIconClickHandler
3.5.2.1.28. 选项组
这是一个允许用户从选项列表中进行选择的组件。单选框用于选择单个值;一组复选框用于选择多个值。
该组件对应的 XML 名称: optionsGroup
-
使用
OptionsGroup
的最简单的情况是为实体属性选择枚举值。例如,Role
实体具有RoleType
类型的type
属性,type
属性就是一个枚举值。就可以使用OptionsGroup
来编辑这个属性,如下所示:<dsContext> <datasource id="roleDs" class="com.haulmont.cuba.security.entity.Role" view="_local"/> </dsContext> <layout> <optionsGroup datasource="roleDs" property="type"/> </layout>
上面的示例中为
Role
实体定义了数据源 :roleDs
。在optionsGroup
组件中,指向数据源的链接在 datasource 属性中指定,实体属性的名称在 property 属性中设置。组件的显示效果:
-
组件选项列表可通过
setOptionsList()
、setOptionsMap()
和setOptionsEnum()
方法任意地指定,或者使用optionsDatasource
属性来指定。
-
setOptionsList()
方法允许以编程方式指定组件选项列表。为此,在 XML 描述中声明一个组件:<optionsGroup id="numberOfSeatsField"/>
然后将该组件注入控制器,并在
init()
方法中指定选项列表:@Inject protected OptionsGroup numberOfSeatsField; @Override public void init(Map<String, Object> params) { List<Integer> list = new ArrayList<>(); list.add(2); list.add(4); list.add(5); list.add(7); numberOfSeatsField.setOptionsList(list); }
该组件将会如下显示:
根据所选的选项,组件的
getValue()
方法将返回Integer
类型的值:2 、 4 、 5 、 7。
-
setOptionsMap()
方法允许分别指定选项的字符串名称和选项值。例如,我们可以在控制器的init()
方法中为已经在 XML 描述中配置的numberOfSeatsField
组件设置以下选项 map:@Inject protected OptionsGroup numberOfSeatsField; @Override public void init(Map<String, Object> params) { Map<String, Object> map = new LinkedHashMap<>(); map.put("two", 2); map.put("four", 4); map.put("five", 5); map.put("seven", 7); numberOfSeatsField.setOptionsMap(map); }
该组件将会如下显示:
根据所选的选项,该组件的
getValue()
方法将返回Integer
类型的值:2、 4 、 5、 7,而不是界面上显示的字符串。
-
setOptionsEnum()
方法将一个枚举类作为参数。选项列表中将显示枚举值的本地化名称,而组件的值将是枚举值。 -
该组件可以从数据源中获取选项列表。为此,需要使用 optionsDatasource 属性。例如:
<dsContext> <collectionDatasource id="coloursDs" class="com.company.sample.entity.Colour" view="_local"> <query>select c from sample$Colour c</query> </collectionDatasource> </dsContext> <layout> <optionsGroup id="coloursField" optionsDatasource="coloursDs"/> </layout>
在这种情况下,
coloursField
组件将显示coloursDs
数据源中的Colour
实体的实例名,它的getValue()
方法将返回所选的实体实例。使用 captionProperty 属性,可以指定一个实体属性作为选项的显示名称。
-
multiselect
属性用于将OptionsGroup
转换为多选模式。如果启用multiselect
,组件将显示为一组独立的复选框,组件值是所选选项的列表。例如,在 XML 描述中创建该组件:
<optionsGroup id="roleTypesField" multiselect="true"/>
并为其设置一个选项列表 –
RoleType
枚举值:@Inject protected OptionsGroup roleTypesField; @Override public void init(Map<String, Object> params) { roleTypesField.setOptionsList(Arrays.asList(RoleType.values())); }
那么该组件将会如下显示:
在这种情况下,组件的
getValue()
方法将返回一个java.util.List
,其中包含RoleType.READONLY
和RoleType.DENYING
枚举值。上面的示例同时展示了
OptionsGroup
组件显示数据模型中枚举值的本地化名称的功能。还可以通过将
java.util.List
值传递给setValue()
方法以编程方式选择一些值:optionsGroup.setValue(Arrays.asList(RoleType.STANDARD, RoleType.ADMIN));
-
orientation
属性定义了分组元素的排列方向。默认情况下元素垂直排列。可以使用horizontal
值设置为水平方向。
- optionsGroup 的属性
-
align - caption - captionAsHtml - captionProperty - contextHelpText - contextHelpTextHtmlEnabled - css - datasource - description - descriptionAsHtml - editable - enable - box.expandRatio - icon - id - multiselect - height - optionsDatasource - optionsEnum - orientation - property - required - requiredMessage - stylename - tabIndex - visible - width
- optionsGroup 的元素
- API
-
addValueChangeListener - setContextHelpIconClickHandler - setOptionsEnum - setOptionsList - setOptionsMap
3.5.2.1.29. 选项列表
OptionsList
是 OptionsGroup 组件的变体,它将选项列表展示为可垂直滚动的列表。如果启用了多选,则可以通过单击时按住 Ctrl 键来选择多个选项,或按住 Shift 键来选择一个范围内的选项。
该组件对应的 XML 名称: optionsList
默认情况下,OptionsList
组件在建议弹窗中显示第一个空元素,可以通过将 nullOptionVisible
属性设置为 false
来禁止此行为。
OptionsList
和 OptionsGroup API 之间的唯一区别是 OptionsList
没有 orientation
属性。
- optionsList 的属性
-
align - caption - captionAsHtml - captionProperty - contextHelpText - contextHelpTextHtmlEnabled - css - datasource - description - descriptionAsHtml - editable - enable - box.expandRatio - height - icon - id - multiselect - nullOptionVisible - optionsDatasource - optionsEnum - property - required - requiredMessage - stylename - tabIndex - visible - width
- optionsList 的元素
- API
3.5.2.1.30. 密码字段
这是一个将用户输入字符显示为回显字符(echo characters)的字段。
该组件的 XML 名称: passwordField
除了不能设置 datatype
,PasswordField
与 TextField 基本一样。PasswordField
仅用于处理文本和 String
类型实体属性。
示例:
<passwordField id="passwordField" caption="msg://name"/>
<button id="showPasswordBtn" caption="msg://buttonsName"/>
@Inject
private PasswordField passwordField;
@Inject
private Notifications notifications;
@Subscribe("showPasswordBtn")
protected void onShowPasswordBtnClick(Button.ClickEvent event) {
notifications.create()
.withCaption(passwordField.getValue())
.show();
}
autocomplete
属性允许在 Web 浏览器中保存密码。默认不保存。
通过 capsLockIndicator
属性设置 CapsLockIndicator组件的 id
,该组件指示 passwordField
的大小写锁定状态。此状态仅在 passwordField
获得焦点时处理。当失去焦点时,状态变为 "Caps Lock off"。
示例:
<passwordField id="passwordField"
capsLockIndicator="capsLockIndicator"/>
<capsLockIndicator id="capsLockIndicator"
align="MIDDLE_CENTER"
capsLockOffMessage="Caps Lock is OFF"
capsLockOnMessage="Caps Lock is ON"/>
- passwordField 的属性
-
align - autocomplete - capsLockIndicator - caption - captionAsHtml - contextHelpText - contextHelpTextHtmlEnabled - css - dataContainer - datasource - description - descriptionAsHtml - editable - enable - box.expandRatio - height - icon - id - maxLength - property - required - requiredMessage - stylename - tabIndex - visible - width
- passwordField 的元素
- API
3.5.2.1.31. 选择器控件
PickerField
在文本字段中显示实体实例,并在用户单击右侧的按钮时执行操作。
该组件的 XML 名称: pickerField
-
PickerField
有一个使用规则,就是它只用于引用类型的实体属性。使用时为组件指定 dataContainer 和 property 属性就可以了:<data> <instance id="carDc" class="com.haulmont.sample.core.entity.Car" view="carEdit"> <loader/> </instance> </data> <layout> <pickerField dataContainer="carDc" property="color"/> </layout>
在上面的例子中,界面为具有
color
属性的Car
实体定义了 id 为carDc
的数据容器。在pickerField
元素中,通过dataContainer
属性连接到此数据容器,并给property
属性设置了实体属性的名称。实体属性应该引用另一个实体,在上面的示例中就是Color
实体。
-
对于
PickerField
,可以定义任意数量的操作,这些操作在组件右侧显示为按钮。操作的定义可以使用
actions
嵌套元素在 XML 描述中完成,也可以使用addAction()
方法在控制器中以编程方式完成。-
平台提供一组标准的 PickerField 操作 :
picker_lookup
、picker_clear
、picker_open
。它们分别执行关联实体的选择、清空组件以及打开所选关联实体的编辑界面。在 XML 中声明标准操作时,应当定义操作的标识符并使用type
属性定义操作类型。如果在声明组件时未定义
actions
元素中的动作,则 XML 加载器将默认为其定义lookup
和clear
操作。要添加一个默认操作,比如open
,就需要定义actions
元素,如下所示:<pickerField dataContainer="carDc" property="color"> <actions> <action id="lookup" type="picker_lookup"/> <action id="open" type="picker_open"/> <action id="clear" type="picker_clear"/> </actions> </pickerField>
action
元素能不能扩展,但可以按操作标识符来覆盖一组标准操作。所以必须明确定义所有需要的操作的标识符。该组件如下所示:使用
addAction()
以编程方式设置标准操作。如果在组件的 XML 描述中没有actions
嵌套元素,就可以使用这个方法添加缺少的操作:@Inject protected PickerField<Color> colorField; @Subscribe protected void onInit(InitEvent event) { colorField.addAction(actions.create(OpenAction.class)); }
如果组件是在控制器中创建的,则它将不会包含默认操作,需要显式添加所有需要的操作:
@Inject private InstanceContainer<Car> carDc; @Inject private UiComponents uiComponents; @Inject private Actions actions; @Subscribe protected void onInit(InitEvent event) { PickerField<Color> colorField = uiComponents.create(PickerField.NAME); colorField.setValueSource(new ContainerValueSource<>(carDc, "color")); colorField.addAction(actions.create(LookupAction.class)); colorField.addAction(actions.create(OpenAction.class)); colorField.addAction(actions.create(ClearAction.class)); getWindow().add(colorField); }
可以在编程时使用相应的组件事件来对标准操作进行参数化。比如,可以通过如下方式设置特定的查找界面 ID:
@Inject private LookupScreens lookupScreens; @Inject private PickerField<Color> pickerField; @Subscribe("pickerField.lookup") protected void onPickerFieldLookupActionPerformed(Action.ActionPerformedEvent event) { lookupScreens.builder(pickerField) .withScreen("custom-lookup") .build() .show(); }
更多信息,请参阅 打开界面 部分。
-
可以在 XML 描述中的
actions
嵌套元素中定义任何操作,这些操作的逻辑可以在操作的事件中实现,例如:<pickerField dataContainer="orderDc" property="customer"> <actions> <action id="lookup"/> <action id="show" icon="PICKERFIELD_OPEN" caption="Show"/> </actions> </pickerField>
@Inject private PickerField<Customer> pickerField; @Subscribe("pickerField.show") protected void onPickerFieldShowActionPerformed(Action.ActionPerformedEvent event) { CustomerEdit customerEdit = editorScreens.builder(pickerField) .withScreen(CustomerEdit.class) .build(); customerEdit.setDiscount(true); customerEdit.show(); }
操作的声明式创建和编程式创建在操作以及操作接口部分有描述。
-
-
可以在不绑定实体的情况下使用
PickerField
,即不设置 dataContainer/datasource 和 property属性。在这种情况下,metaClass
属性应该用于指定PickerField
的实体类型。例如:<pickerField id="colorField" metaClass="sample$Color"/>
可以通过将组件注入控制器并调用其
getValue()
方法来获取所选实体的实例。要正确使用
PickerField
组件,需要设置metaClass
属性,或者同时设置 dataContainer/datasource 和property 属性。可以在
PickerField
中使用键盘快捷键,有关详细信息,请参阅快捷键。
- pickerField 的属性
-
align - caption - captionAsHtml - captionProperty - contextHelpText - contextHelpTextHtmlEnabled - css - dataContainer - datasource - description - descriptionAsHtml - editable - enable - box.expandRatio - height - icon - id - metaClass - property - required - requiredMessage - stylename - tabIndex - visible - width
- pickerField 的元素
- API
-
addAction - addValueChangeListener - commit - discard - isModified - setContextHelpIconClickHandler
3.5.2.1.32. 弹窗按钮
这是一个带弹窗的按钮。弹窗中可以包含操作列表或自定义内容。
该组件的 XML 名称: popupButton
PopupButton
可以使用caption属性指定按钮名称,使用icon属性指定按钮图标。使用description属性定义提示文字。下图显示了不同类型的按钮:
popupButton
的元素:
-
actions
- 指定下拉列表内的操作。操作的属性中只有
caption
、enable
、visible
能起作用。description
和shortcut
属性会被忽略。icon
属性的处理取方式决于应用程序属性 cuba.gui.showIconsForPopupMenuActions 和组件的showActionIcons
属性。后者优先。下面是一个按钮示例,其中包含一个具有两个操作的下拉列表:
<popupButton id="popupButton" caption="msg://popupButton" description="Press me"> <actions> <action id="popupAction1" caption="msg://action1"/> <action id="popupAction2" caption="msg://action2"/> </actions> </popupButton>
可以定义新的操作,也可以使用当前界面中元素已定义的操作,例如:
<popupButton id="popupButton"> <actions> <action id="ordersTable.create"/> <action id="ordersTable.edit"/> <action id="ordersTable.remove"/> </actions> </popupButton>
-
popup
- 为弹窗设置自定义的内容。如果设置了自定义弹出内容,则会忽略操作。下面是自定义弹出布局的示例:
<popupButton id="popupButton" caption="Settings" align="MIDDLE_CENTER" icon="font-icon:GEARS" closePopupOnOutsideClick="true" popupOpenDirection="BOTTOM_CENTER"> <popup> <vbox width="250px" height="AUTO" spacing="true" margin="true"> <label value="Settings" align="MIDDLE_CENTER" stylename="h2"/> <progressBar caption="Progress" width="100%"/> <textField caption="New title" width="100%"/> <lookupField caption="Status" optionsEnum="com.haulmont.cuba.core.global.SendingStatus" width="100%"/> <hbox spacing="true"> <button caption="Save" icon="SAVE"/> <button caption="Reset" icon="REMOVE"/> </hbox> </vbox> </popup> </popupButton>
popupButton
的属性:
-
autoClose
- 定义是否应在操作触发后自动关闭弹窗。
-
closePopupOnOutsideClick
- 如果设置为true
,则单击弹窗外部时将其关闭。这不会影响单击按钮本身的行为。
-
popupOpenDirection
- 设置弹窗的打开方向。可能的取值:-
BOTTOM_LEFT
, -
BOTTOM_RIGHT
, -
BOTTOM_CENTER
.
-
-
showActionIcons
- 显示操作按钮的图标。
-
togglePopupVisibilityOnClick
- 定义在弹窗上连续点击是否切换弹窗可见性。
PopupButton
接口的方法:
-
addPopupVisibilityListener()
- 添加一个监听器来拦截组件的可见性更改事件。popupButton.addPopupVisibilityListener(popupVisibilityEvent -> notifications.create() .withCaption("Popup visibility changed") .show());
也可以通过订阅相应事件来跟踪
PopupButton
的可见性状态更改。@Subscribe("popupButton") protected void onPopupButtonPopupVisibility(PopupButton.PopupVisibilityEvent event) { notifications.create() .withCaption("Popup visibility changed") .show(); }
- popupButton 的属性
-
align - autoClose - caption - captionAsHtml - closePopupOnOutsideClick - css - description - descriptionAsHtml - enable - box.expandRatio - icon - id - menuWidth - popupOpenDirection - showActionIcons - stylename - tabIndex - togglePopupVisibilityOnClick - visible - width
- popupButton 的元素
- API
3.5.2.1.33. 弹窗查看控件
PopupView
是一个允许用容器打开弹出窗口的控件。可以通过单击简要值或以编程方式打开弹窗。可以通过鼠标移出或点击外部区域来关闭弹窗。
典型的 PopupView
如下所示:
从本地化消息包中获取简要值的 PopupView 的示例:
<popupView id="popupView"
minimizedValue="msg://minimizedValue"
caption="PopupView caption">
<vbox width="60px" height="40px">
<label value="Content" align="MIDDLE_CENTER"/>
</vbox>
</popupView>
PopupView
的内部内容应该是一个容器,例如 BoxLayout。
PopupView
方法:
-
setPopupVisible()
允许以编程方式打开弹窗。@Inject private PopupView popupView; @Subscribe protected void onInit(InitEvent event) { popupView.setMinimizedValue("Hello world!"); }
-
setMinimizedValue()
允许以编程方式设置简要值。@Inject private PopupView popupView; @Override public void init(Map<String, Object> params) { popupView.setMinimizedValue("Hello world!"); }
-
addPopupVisibilityListener(PopupVisibilityListener listener)
方法可用来添加一个跟踪弹窗可见性变化的监听器。@Inject private PopupView popupView; @Inject private Notifications notifications; @Subscribe protected void onInit(InitEvent event) { popupView.addPopupVisibilityListener(popupVisibilityEvent -> notifications.create() .withCaption(popupVisibilityEvent.isPopupVisible() ? "The popup is visible" : "The popup is hidden") .withType(Notifications.NotificationType.HUMANIZED) .show() ); }
PopupView
的属性:
-
minimizedValue
属性定义弹窗按钮的简要值文本。此文本可包含 HTML 标记。
-
如果
hideOnMouseOut
属性设置为false
,在弹窗外部单击时会关闭弹窗。
- popupView 的属性
-
caption - captionAsHtml - contextHelpText - contextHelpTextHtmlEnabled - css - description - descriptionAsHtml - box.expandRatio - height - hideOnMouseOut - icon - id - minimizedValue - stylename - visible - width
- API
3.5.2.1.34. 进度条
ProgressBar
组件用于显示需要长时间处理的任务的进度。
该组件的 XML 名称: progressBar
下面是该组件与后台任务机制一起使用的示例:
<progressBar id="progressBar" width="100%"/>
@Inject
private ProgressBar progressBar;
@Inject
private BackgroundWorker backgroundWorker;
private static final int ITERATIONS = 5;
@Subscribe
protected void onInit(InitEvent event){
BackgroundTask<Integer, Void> task = new BackgroundTask<Integer, Void>(300, getWindow()) {
@Override
public Void run(TaskLifeCycle<Integer> taskLifeCycle) throws Exception{
for(int i = 1; i <= ITERATIONS; i++) {
TimeUnit.SECONDS.sleep(2); (1)
taskLifeCycle.publish(i);
}
return null;
}
@Override
public void progress(List<Integer> changes){
double lastValue = changes.get(changes.size() - 1);
progressBar.setValue((lastValue / ITERATIONS));
}
};
BackgroundTaskHandler taskHandler = backgroundWorker.handle(task);
taskHandler.execute();
}
1 | 一些比较耗时的任务 |
BackgroundTask.progress()
方法在 UI 线程中被执行,在这个方法里给 ProgressBar
组件设置当前的进度值。组件值应该是从 0.0
到 1.0
的 double
类型的数值。
可以使用 ValueChangeListener
跟踪 ProgressBar
值的变化。可以使用 isUserOriginated() 方法跟踪 ValueChangeEvent
的来源。
如果正在运行的处理无法发送有关进度的信息,则可以显示表示不确定状态的指示符。将 indeterminate
设置为 true
以显示不确定状态。默认为 false
。例如:
<progressBar id="progressBar" width="100%" indeterminate="true"/>
默认情况下,不定进度条显示为水平状态条。要改为显示螺旋状的进度条,可以设置属性 stylename="indeterminate-circle"
。
要使进度条指示器显示为在进度条上移动的点(而不是增长条),请使用 point
预定义样式:
progressBar.setStyleName(HaloTheme.PROGRESSBAR_POINT);
- progressBar 的属性
-
align - caption - captionAsHtml - contextHelpText - contextHelpTextHtmlEnabled - css - description - descriptionAsHtml - enable - box.expandRatio - height - id - indeterminate - stylename - visible - width
- progressBar 的预定义样式
- API
3.5.2.1.35. 单选按钮组
这是一个允许用户使用单选按钮从选项列表中选择单个值的组件。
该组件对应的 XML 名称: radioButtonGroup
可以使用 setOptions()
、setOptionsList()
、 setOptionsMap()
和 setOptionsEnum()
方法,或使用 optionsDatasource
或 optionsContainer
属性指定组件选项列表。
-
使用
RadioButtonGroup
的最简单的场景是为实体属性选择枚举值。例如,Role
实体具有RoleType
类型的type
属性,它是一个枚举。那么可以使用RadioButtonGroup
显示这个属性, 如下所示:<radioButtonGroup optionsEnum="com.haulmont.cuba.security.entity.RoleType" property="type"/>
setOptionsEnum()
将一个枚举类作为参数。选项列表将包含枚举值的本地化名称,组件的值将是一个枚举值。radioButtonGroup.setOptionsEnum(RoleType.class);
使用
setOptions()
方法可以得到相同的结果,该方法允许使用任何类型的选项:radioButtonGroup.setOptions(new EnumOptions<>(RoleType.class));
-
setOptionsList()
能够以编程方式指定组件选项列表。为此在 XML 描述中声明一个组件:<radioButtonGroup id="radioButtonGroup"/>
然后将组件注入控制器并为其指定选项列表:
@Inject private RadioButtonGroup<Integer> radioButtonGroup; @Subscribe protected void onInit(InitEvent event) { List<Integer> list = new ArrayList<>(); list.add(2); list.add(4); list.add(5); list.add(7); radioButtonGroup.setOptionsList(list); }
该组件将如下所示:
根据所选的选项,组件的
getValue()
方法将返回Integer
类型的值:2 、4 、5 、7。
-
setOptionsMap()
能够分别指定选项的显示名称和选项值。例如,我们可以为控制器中注入的radioButtonGroup
组件设置以下选项 map:@Inject private RadioButtonGroup<Integer> radioButtonGroup; @Subscribe protected void onInit(InitEvent event) { Map<String, Integer> map = new LinkedHashMap<>(); map.put("two", 2); map.put("four", 4); map.put("five", 5); map.put("seven", 7); radioButtonGroup.setOptionsMap(map); }
该组件将如下所示:
根据所选的选项,组件的
getValue()
方法将返回Integer
类型的值:2 、4 、5 、 7,而不是界面上显示的字符串。
-
该组件可以从数据容器中获取选项列表。要做到这点,需要使用
optionsContainer
属性。例如:<data> <collection id="employeesCt" class="com.company.demo.entity.Employee" view="_minimal"> <loader> <query><![CDATA[select e from demo_Employee e]]></query> </loader> </collection> </data> <layout> <radioButtonGroup optionsContainer="employeesCt"/> </layout>
在这种情况下,
radioButtonGroup
组件将显示位于employeesCt
数据容器中的Employee
实体的实例名,其getValue()
方法将返回所选实体实例。使用captionProperty属性,可以指定一个实体属性作为选项的显示名称,而不是使用实例名称作为选项的显示名称。
可以使用
RadioButtonGroup
接口的setOptions()
方法以编程方式定义选项容器:@Inject private RadioButtonGroup<Employee> radioButtonGroup; @Inject private CollectionContainer<Employee> employeesCt; @Subscribe protected void onInit(InitEvent event) { radioButtonGroup.setOptions(new ContainerOptions<>(employeesCt)); }
orientation
属性定义了分组元素的排列方向。默认情况下,元素垂直排列。设置值为 horizontal
将水平排列。
- RadioButtonGroup 的属性
-
align - box.expandRatio - caption - captionAsHtml - captionProperty - colspan - contextHelpText - contextHelpTextHtmlEnabled - css - dataContainer - datasource - description - descriptionAsHtml - editable - enable - height - icon - id - optionsContainer - optionsDatasource - optionsEnum - orientation - property - required - requiredMessage - responsive - rowspan - stylename - tabIndex - visible - width
- API
-
addValueChangeListener - setContextHelpIconClickHandler - setOptions - setOptionsEnum - setOptionsList - setOptionsMap
3.5.2.1.36. 关联实体组件
RelatedEntities
组件是一个弹窗按钮,其中包含了与表格中显示的实体相关的实体类的下拉列表。当用户选择了所需的实体类,就会打开一个新的浏览界面,其中包含与初始表格中选择的实体实例关联的实体实例。
该组件的 XML 名称: relatedEntities
选中的关联实体受用户的实体、实体属性和界面权限机制的控制。
默认情况下,下拉列表中所选类的浏览界面使用约定的格式( {entity_name}.browse
、 {entity_name}.lookup
)定义。当然,也可以在组件中显式自定义浏览界面。
在新的浏览界面中会动态创建过滤器,这个过滤器只选择与选中实体相关的记录。
<table id="invoiceTable"
multiselect="true"
width="100%">
<actions>
<action id="create"/>
<action id="edit"/>
<action id="remove"/>
</actions>
<buttonsPanel id="buttonsPanel">
<button id="createBtn"
action="invoiceTable.create"/>
<button id="editBtn"
action="invoiceTable.edit"/>
<button id="removeBtn"
action="invoiceTable.remove"/>
<relatedEntities for="invoiceTable"
openType="NEW_TAB">
<property name="invoiceItems"
screen="sales$InvoiceItem.lookup"
filterCaption="msg://invoiceItems"/>
</relatedEntities>
</buttonsPanel>
. . .
</table>
for
属性是必须的。使用这个属性指定要查看其关联实体的表格的标识符。
openType="NEW_TAB"
属性将查找窗口的打开模式设置为新标签页。默认情况下,实体浏览界面在当前标签页中打开。
property
元素允许显式定义显示在下拉列表中的相关实体。
property
元素的属性:
-
name
– 当前实体的属性名称,这个属性是一个引用类型的属性,引用了关联实体。
-
screen
– 要使用的浏览界面的标识符。
-
filterCaption
– 动态生成的过滤器的标题。
可以使用 exclude
属性从下拉列表中排除一些关联实体。该属性的值是匹配要排除的引用属性的正则表达。
平台提供了一个不使用 RelatedEntities
组件就可以打开关联实体界面的 API:RelatedEntitiesAPI
接口及其实现 RelatedEntitiesBean
。逻辑是在 openRelatedScreen()
方法定义的,该方法可接受三个参数:关系一侧的实体集合、该集合中单个实体的 MetaClass
、要查找其关联实体的字段。
<button id="related"
caption="Related customer"/>
@UiController("sales_Order.browse")
@UiDescriptor("order-browse.xml")
@LookupComponent("ordersTable")
@LoadDataBeforeShow
public class OrderBrowse extends StandardLookup<Order> {
@Inject
private RelatedEntitiesAPI relatedEntitiesAPI;
@Inject
private GroupTable<Order> ordersTable;
@Subscribe("related")
protected void onRelatedClick(Button.ClickEvent event) {
relatedEntitiesAPI.openRelatedScreen(ordersTable.getSelected(), Order.class, "customer");
}
}
默认情况下,将打开标准实体浏览界面。可以使用 RelatedScreenDescriptor
参数使该方法打开另一个界面或使用其它参数打开界面。RelatedScreenDescriptor
是一个 POJO,可以存储界面标识符(String
)、打开类型(WindowManager.OpenType
)、过滤器标题(String
)和界面参数(Map <String,Object>
)。
relatedEntitiesAPI.openRelatedScreen(ordersTable.getSelected(),
Order.class, "customer",
new RelatedEntitiesAPI.RelatedScreenDescriptor("sales$Customer.lookup", WindowManager.OpenType.DIALOG));
- relatedEntities 的属性
-
align - caption - captionAsHtml - css - description - descriptionAsHtml - enable - exclude - box.expandRatio - for - icon - id - openType - stylename - tabIndex - visible - width
- property 的属性
-
caption - filterCaption - name - screen
3.5.2.1.37. 可调大小文本区
ResizableTextArea
是一个多行文本编辑器空间,具有能调整该组件大小的能力。
该组件的 XML 名称: resizableTextArea
ResizableTextArea
基本复制了文本区组件的功能,但是有下面特殊的属性:
-
resizableDirection
– 定义用户能改变该组件大小的方式,当该组件的大小用百分比定义时除外。<textArea id="textArea" resizableDirection="BOTH"/>
有四种调整大小的模式:
-
BOTH
– 组件可以在两个方向调整大小。BOTH
是默认值。如果组件大小设置的是百分比,则组件大小不可调整。 -
NONE
– 组件大小不可调整。 -
VERTICAL
– 组件只能在竖直方向调整大小。如果组件大小设置的是百分比,则组件大小竖直方向不可调整。 -
HORIZONTAL
– 组件只能在水平方向调整大小。如果组件大小设置的是百分比,则组件大小水平方向不可调整。
区域尺寸更改的事件可以通过
ResizeListener
接口跟踪。示例:resizableTextArea.addResizeListener(resizeEvent -> notifications.create() .withCaption("Resized") .show());
-
- resizableTextArea 属性
-
align - caption - captionAsHtml - caseConversion - cols - contextHelpText - contextHelpTextHtmlEnabled - css - dataContainer - datasource - datatype - description - descriptionAsHtml - editable - enable - box.expandRatio - height - icon - id - inputPrompt - maxLength - property - required - requiredMessage - responsive - rows - settingsEnabled - stylename - tabIndex - textChangeEventMode - textChangeTimeout - trim - visible - width - wordwrap
- resizableTextArea 的预定义样式
-
align-center - align-right - borderless - huge - large - small - tiny
- API
-
addResizeListener - addTextChangeListener - addValueChangeListener - addValidator - applySettings - commit - discard - isModified - saveSettings - setContextHelpIconClickHandler
3.5.2.1.38. 富文本区
这是一个用于显示和输入带有格式的文本的文本区域。
该组件的 XML 名称: richTextArea
基本上,RichTextArea
与 TextField 的功能一致,除了不能为它设置 datatype
。因此,RichTextArea
仅适用于文本和 String
类型的实体属性。
- richTextArea 的属性
-
align - caption - captionAsHtml - contextHelpText - contextHelpTextHtmlEnabled - css - dataContainer - datasource - description - descriptionAsHtml - editable - enable - box.expandRatio - height - icon - id - property - required - requiredMessage - stylename - tabIndex - visible - width
- API
3.5.2.1.39. 搜索选择器控件
SearchPickerField
组件用于根据输入的字符串搜索实体实例。用户可输入几个字符,然后按 Enter 键。如果找到了多个匹配项,则所有匹配项都将显示在下拉列表中。如果只有一个实例与搜索关键字匹配,则这个实例直接成为组件值。 还可以通过单击 SearchPickerField
组件右侧的按钮来执行操作。
该组件的 XML 名称: searchPickerField
-
要使用
SearchPickerField
组件,需要创建 collectionDatasource 并指定一个包含相应搜索条件的查询。条件必须包含名为custom$searchString
的参数。此参数将被填充为用户输入的搜索关键字。应将组件的 optionsDatasource 属性设置为带有搜索条件的数据源。例如:<dsContext> <datasource id="carDs" class="com.company.sample.entity.Car" view="_local"/> <collectionDatasource id="colorsDs" class="com.company.sample.entity.Color" view="_local"> <query> select c from sample$Color c where c.name like :(?i)custom$searchString </query> </collectionDatasource> </dsContext> <layout> <searchPickerField datasource="carDs" property="color" optionsDatasource="colorsDs"/> </layout>
这时,组件会根据
Colour
实体的name
属性中是否包含搜索关键字来查找Colour
实体的实例。(?i)
前缀用于不区分大小写查找(请参阅不区分大小写查找)。选择的值将设置到carDs
数据源中的Car
实体的colour
属性。escapeValueForLike
属性设置为true
时允许使用 like 子句搜索特殊符号:%
、\
和_
。要使用escapeValueForLike = true
,修改集合数据源的查询,为其添加转义符:select c from ref$Colour c where c.name like :(?i)custom$searchString or c.description like :(?i)custom$searchString escape '\'
escapeValueForLike
属性适用于除 HSQLDB 之外的所有数据库。
-
使用
minSearchStringLength
属性,设置要执行搜索应输入的最小字符数。 -
在界面控制器中,可以使用
SearchField.SearchNotifications
类给用户显示一些搜索提示,需要实现这个类的两个方法:-
如果输入的字符数小于
minSearchStringLength
属性的值时调用的方法。 -
如果用户输入的字符没有返回任何结果时调用的方法。
下面是这两个方法的实现示例:
@Inject private Notifications notifications; @Inject private SearchPickerField colorField; @Subscribe protected void onInit(InitEvent event) { colorField.setSearchNotifications(new SearchField.SearchNotifications() { @Override public void notFoundSuggestions(String filterString) { notifications.create() .withCaption("No colors found for search string: " + filterString) .withType(Notifications.NotificationType.TRAY) .show(); } @Override public void needMinSearchStringLength(String filterString, int minSearchStringLength) { notifications.create() .withCaption("Minimum length of search string is " + minSearchStringLength) .withType(Notifications.NotificationType.TRAY) .show(); } }); }
-
-
SearchPickerField
实现了 LookupField 和 PickerField 接口。除了在 XML 中定义组件时添加的默认操作列表(对于SearchPickerField
,默认操作是lookup
和open
操作)不同,其它功能与LookupField
和PickerField
接口定义的功能相同。
- searchPickerField 的属性
-
align - caption - captionAsHtml - captionProperty - contextHelpText - contextHelpTextHtmlEnabled - css - datasource - description - descriptionAsHtml - editable - enable - escapeValueForLike - box.expandRatio - filterMode - height - icon - id - inputPrompt - metaClass - minSearchStringLength - newOptionAllowed - newOptionHandler - nullName - optionsDatasource - property - required - requiredMessage - stylename - tabIndex - visible - width
- searchPickerField 的元素
- searchPickerField 的预定义样式
- API
3.5.2.1.40. 侧边菜单
SideMenu
组件提供了定制主窗口布局、管理菜单项、添加图标和标记(badges)以及应用自定义样式的方法。
它也可以像其它可视化组件一样用在任何界面中。要将 SideMenu
组件添加到界面,应该将 xmlns:main ="http://schemas.haulmont.com/cuba/mainwindow.xsd"
命名空间添加到界面描述中。
该组件的 XML 名称: sideMenu
在界面 XML 描述中定义该组件的示例:
<main:sideMenu id="sideMenu"
width="100%"
selectOnClick="true"/>
CUBA Studio 为主窗口提供了界面模板,其中包含 sideMenu
组件和侧边面板中的预定义样式:
<layout>
<hbox id="horizontalWrap"
expand="workArea"
height="100%"
stylename="c-sidemenu-layout"
width="100%">
<vbox id="sideMenuPanel"
expand="sideMenu"
height="100%"
margin="false,false,true,false"
spacing="true"
stylename="c-sidemenu-panel"
width="250px">
<hbox id="appTitleBox"
spacing="true"
stylename="c-sidemenu-title"
width="100%">
<label id="appTitleLabel"
align="MIDDLE_CENTER"
value="mainMsg://application.logoLabel"/>
</hbox>
<embedded id="logoImage"
align="MIDDLE_CENTER"
stylename="c-app-icon"
type="IMAGE"/>
<hbox id="userInfoBox"
align="MIDDLE_CENTER"
expand="userIndicator"
margin="true"
spacing="true"
width="100%">
<main:userIndicator id="userIndicator"
align="MIDDLE_CENTER"/>
<main:newWindowButton id="newWindowButton"
description="mainMsg://newWindowBtnDescription"
icon="app/images/new-window.png"/>
<main:logoutButton id="logoutButton"
description="mainMsg://logoutBtnDescription"
icon="app/images/exit.png"/>
</hbox>
<main:sideMenu id="sideMenu"
width="100%"/>
<main:ftsField id="ftsField"
width="100%"/>
</vbox>
<main:workArea id="workArea"
height="100%">
<main:initialLayout margin="true"
spacing="true">
<label id="welcomeLabel"
align="MIDDLE_CENTER"
stylename="c-welcome-text"
value="mainMsg://application.welcomeText"/>
</main:initialLayout>
</main:workArea>
</hbox>
</layout>
sideMenu 属性:
-
selectOnClick
属性设置为true
时,会在鼠标单击时突出显示选中的菜单项。默认值为false
。
SideMenu 接口的方法:
-
createMenuItem
- 创建一个新菜单项,但不将此项添加到菜单。对于整个菜单,Id
必须是唯一的。
-
addMenuItem
- 添加菜单项到菜单。
-
removeMenuItem
- 从菜单项列表中移除菜单项。 -
getMenuItem
- 根据id
从菜单树中获取菜单项。 -
hasMenuItems
- 如果菜单包含菜单项,则返回true
。
SideMenu
组件用于显示菜单项。MenuItem
API 允许在界面控制器中创建菜单项。以下方法可用于根据应用程序业务逻辑动态更新菜单项。以编程方式添加菜单项的示例:
SideMenu.MenuItem item = sideMenu.createMenuItem("special");
item.setCaption("Daily offer");
item.setBadgeText("New");
item.setIconFromSet(CubaIcon.GIFT);
sideMenu.addMenuItem(item,0);
MenuItem 接口的方法:
-
setCaption
- 设置菜单项名称。
-
setCaptionAsHtml
- 启用或禁用 HTML 模式的菜单名称。
-
setBadgeText
- 设置菜单项的标记文本。标记是显示在菜单项右侧的小部件,例如:int count = 5; SideMenu.MenuItem item = sideMenu.createMenuItem("count"); item.setCaption("Messages"); item.setBadgeText(count + " new"); item.setIconFromSet(CubaIcon.ENVELOPE); sideMenu.addMenuItem(item,0);
标记文本可以在 Timer 组件的配合下动态更新:
public void updateCounters(Timer source) { sideMenu.getMenuItemNN("sales") .setBadgeText(String.valueOf(LocalTime.MIDNIGHT.minusSeconds(timerCounter-source.getDelay()))); timerCounter++; }
-
setIcon
- 设置菜单项图标。
-
setCommand
- 设置菜单项命令,或点击菜单项时要执行的操作。
-
addChildItem/removeChildItem
- 添加或移除子菜单的菜单项。
-
setExpanded
- 默认展开或折叠包含子菜单的菜单项。
-
setStyleName
- 给组件设置一个或多个自定义样式名,并且会覆盖所有已定义的用户样式。多个样式通过空格分隔的样式名列表指定。样式名必须是有效的 CSS class 名称。标准的
sideMenu
模板包含一些预定义样式:c-sidemenu-layout
、c-sidemenu-panel
和c-sidemenu-title
。默认的c-sidemenu
样式在Halo
和Hover
这两个主题及它们的扩展主题中支持。
-
setTestId
- 调用用于 UI 测试的cuba-id
值。
- sideMenu 的属性
-
align - caption - css - description - enable - height - icon - id - selectOnClick - stylename - tabIndex - visible - width
- ftsfield 属性
-
align - caption - description - enable - height - icon - id - stylename - visible - width
- sideMenu 的 API
- menuItem 的 API
-
addChildItem - removeChildItem - setBadgeText - setCaption - setCaptionAsHtml - setCommand - setExpanded - setIcon - setStyleName - setTestId
3.5.2.1.41. 源码编辑器
SourceCodeEditor
用于显示和输入源码。它是一个多行文本区域,具有代码高亮显示和可选的打印边距以及带行号的侧栏。
该组件的 XML 元素: sourceCodeEditor
基本上,SourceCodeEditor
主要复制 TextField组件的功能,并具有以下特性:
-
如果
handleTabKey
是true
,Tab 按键被处理为缩进行,当为false
时,它用于移动光标或焦点到下一个制表位。应在初始化界面时设置此属性,此属性不支持运行时更改。
可以在运行时更改所有以下属性:
-
highlightActiveLine
用于高亮光标所在行。
-
mode
提供语法高亮支持的语言列表。此列表在SourceCodeEditor
接口的Mode
枚举中定义,包括以下语言:Java、HTML、XML、Groovy、SQL、JavaScript、Properties 和不进行高亮显示的 Text。
-
printMargin
设置是否显示打印边距。
-
showGutter
用于设置显示行号的侧栏是否隐藏。
下面是在运行时调整 SourceCodeEditor
组件的示例。
XML-descriptor:
<hbox spacing="true">
<checkBox id="highlightActiveLineCheck" align="BOTTOM_LEFT" caption="Highlight Active Line"/>
<checkBox id="printMarginCheck" align="BOTTOM_LEFT" caption="Print Margin"/>
<checkBox id="showGutterCheck" align="BOTTOM_LEFT" caption="Show Gutter"/>
<lookupField id="modeField" align="BOTTOM_LEFT" caption="Mode" required="true"/>
</hbox>
<sourceCodeEditor id="simpleCodeEditor" width="100%"/>
控制器:
@Inject
private CheckBox highlightActiveLineCheck;
@Inject
private LookupField<HighlightMode> modeField;
@Inject
private CheckBox printMarginCheck;
@Inject
private CheckBox showGutterCheck;
@Inject
private SourceCodeEditor simpleCodeEditor;
@Subscribe
protected void onInit(InitEvent event) {
highlightActiveLineCheck.setValue(simpleCodeEditor.isHighlightActiveLine());
highlightActiveLineCheck.addValueChangeListener(e ->
simpleCodeEditor.setHighlightActiveLine(Boolean.TRUE.equals(e.getValue())));
printMarginCheck.setValue(simpleCodeEditor.isShowPrintMargin());
printMarginCheck.addValueChangeListener(e ->
simpleCodeEditor.setShowPrintMargin(Boolean.TRUE.equals(e.getValue())));
showGutterCheck.setValue(simpleCodeEditor.isShowGutter());
showGutterCheck.addValueChangeListener(e ->
simpleCodeEditor.setShowGutter(Boolean.TRUE.equals(e.getValue())));
Map<String, HighlightMode> modes = new HashMap<>();
for (HighlightMode mode : SourceCodeEditor.Mode.values()) {
modes.put(mode.toString(), mode);
}
modeField.setOptionsMap(modes);
modeField.setValue(HighlightMode.TEXT);
modeField.addValueChangeListener(e ->
simpleCodeEditor.setMode(e.getValue()));
}
结果是:
SourceCodeEditor
也支持 Suggester
接口提供的代码自动完成功能。要激活自动完成功能,应调用 setSuggester
方法,例如:
@Inject
protected DataGrid<User> usersGrid;
@Inject
private SourceCodeEditor suggesterCodeEditor;
@Inject
private CollectionContainer<User> usersDc;
@Inject
private CollectionLoader<User> usersDl;
@Subscribe
protected void onInit(InitEvent event) {
suggesterCodeEditor.setSuggester((source, text, cursorPosition) -> {
List<Suggestion> suggestions = new ArrayList<>();
usersDl.load();
for (User user : usersDc.getItems()) {
suggestions.add(new Suggestion(source, user.getLogin(), user.getName(), null, -1, -1));
}
return suggestions;
});
}
结果:
- sourceCodeEditor 的属性
-
align - caption - captionAsHtml - colspan - contextHelpText - contextHelpTextHtmlEnabled - css - dataContainer - datasource - description - descriptionAsHtml - editable - enable - box.expandRatio - handleTabKey - height - highlightActiveLine - icon - id - mode - printMargin - property - required - requiredMessage - rowspan - showGutter - stylename - tabIndex - visible - width
- API
3.5.2.1.42. 建议字段
SuggestionField
组件用于根据用户输入的字符串搜索一些值。它与 SuggestionPickerField 的不同之处在于它可以使用任何类型的选项:例如,实体、字符串或枚举值,并且没有操作按钮。选项列表是根据应用程序开发人员定义的逻辑在后端加载。
该组件的 XML 名称: suggestionField
suggestionField
的属性:
-
asyncSearchDelayMs
- 设置最后一次按键操作和执行异步搜索之前需要的延迟。
-
minSearchStringLength
- 设置执行建议搜索需要的最小字符串长度。
-
popupWidth
- 设置建议选项弹出框的宽度。可选项:
-
auto
- 弹出框的宽度等于建议选项的最大长度,即自适应, -
parent
- 弹出框的宽度等于主组件的宽度, -
绝对(比如
"170px"
)或相对(比如"50%"
)值。
-
-
suggestionsLimit
- 设置可显示的建议选项的最大数量。
suggestionField
的元素:
-
query
- 用于定义获取建议值的可选元素。query
元素有以下属性:-
entityClass
(必须) - 实体类的完全限定名。
-
view
- 可选属性,指定用于加载查询实体的视图。
-
escapeValueForLike
- 允许搜索关键字中包含特殊符号:%
、\
等。默认值为false
。
-
searchStringFormat
- 搜索字符串格式,一个 Groovy 字符串,可以使用任何有效的 Groovy 字符串表达式。
<suggestionField id="suggestionField" captionProperty="login"> <query entityClass="com.haulmont.cuba.security.entity.User" escapeValueForLike="true" view="user.edit" searchStringFormat="%$searchString%"> select e from sec$User e where e.login like :searchString escape '\' </query> </suggestionField>
如果未定义查询,则必须使用
SearchExecutor
提供选项列表,以编程方式分配(见下文)。 -
大部分情况下,给组件设置 SearchExecutor
就足够了。SearchExecutor
是一个包含单个方法的功能接口:List<E> search(String searchString,Map <String,Object> searchParams)
:
suggestionField.setSearchExecutor((searchString, searchParams) -> {
return Arrays.asList(entity1, entity2, ...);
});
SearchExecutor
可以返回任何类型的选项,例如实体、字符串或枚举值。
-
实体:
customersDs.refresh();
List<Customer> customers = new ArrayList<>(customersDs.getItems());
suggestionField.setSearchExecutor((searchString, searchParams) ->
customers.stream()
.filter(customer -> StringUtils.containsIgnoreCase(customer.getName(), searchString))
.collect(Collectors.toList()));
-
字符串:
List<String> strings = Arrays.asList("Red", "Green", "Blue", "Cyan", "Magenta", "Yellow");
stringSuggestionField.setSearchExecutor((searchString, searchParams) ->
strings.stream()
.filter(str -> StringUtils.containsIgnoreCase(str, searchString))
.collect(Collectors.toList()));
-
枚举:
List<SendingStatus> enums = Arrays.asList(SendingStatus.values());
enumSuggestionField.setSearchExecutor((searchString, searchParams) ->
enums.stream()
.map(sendingStatus -> messages.getMessage(sendingStatus))
.filter(str -> StringUtils.containsIgnoreCase(str, searchString))
.collect(Collectors.toList()));
-
OptionWrapper
用于需要将任何类型的值与其字串表示分离的情况下:
List<OptionWrapper> wrappers = Arrays.asList(
new OptionWrapper("One", 1),
new OptionWrapper("Two", 2),
new OptionWrapper("Three", 3);
suggestionField.setSearchExecutor((searchString, searchParams) ->
wrappers.stream()
.filter(optionWrapper -> StringUtils.containsIgnoreCase(optionWrapper.getCaption(), searchString))
.collect(Collectors.toList()));
|
searchString
参数可用于使用用户输入的字符串过滤候选值。也可以使用 escapeForLike()
方法来搜索包含特殊符号的值:
suggestionField.setSearchExecutor((searchString, searchParams) -> {
searchString = QueryUtils.escapeForLike(searchString);
return dataManager.loadList(LoadContext.create(Customer.class).setQuery(
LoadContext.createQuery("select c from sample$Customer c where c.name like :name order by c.name escape '\\'")
.setParameter("name", "%" + searchString + "%")));
});
-
OptionsStyleProvider
允许为suggestionField
的建议选项中的每一项指定单独的样式名。suggestionField.setOptionsStyleProvider((field, item) -> { User user = (User) item; switch (user.getGroup().getName()) { case "Company": return "company"; case "Premium": return "premium"; default: return "company"; } });
- suggestionField 的属性
-
align - asyncSearchDelayMs - caption - captionAsHtml - captionProperty - colspan - contextHelpText - contextHelpTextHtmlEnabled - css - dataContainer - datasource - description - descriptionAsHtml - editable - enable - box.expandRatio - height - icon - id - inputPrompt - minSearchStringLength - popupWidth - property - required - requiredMessage - responsive - rowspan - stylename - suggestionsLimit - tabIndex - visible - width
- suggestionField 的元素
- query 元素的属性
-
entityClass - escapeValueForLike - searchStringFormat - view
- suggestionField 预定义样式
- API
-
addValueChangeListener - setContextHelpIconClickHandler - setOptionsStyleProvider
3.5.2.1.43. 建议选择器字段
SuggestionPickerField
组件用于根据用户输入的字符串搜索实体实例。它与 SearchPickerField 的不同之处在于它在用户输入每个字符时自动刷新选项列表,不需要按 Enter 键。 选项列表是根据应用程序开发人员定义的逻辑在后端加载。
SuggestionPickerField
也是一种PickerField,在右侧可以有操作按钮。
该组件的 XML 名称: suggestionPickerField
SuggestionPickerField
用于选择并引用实体属性,因此通常设置它的 dataContainer
和 property
属性:
<data>
<instance id="orderDc"
class="com.company.sales.entity.Order"
view="order-with-customer">
<loader id="orderDl"/>
</instance>
</data>
<layout>
<suggestionPickerField id="suggestionPickerField"
captionProperty="name"
dataContainer="orderDc"
property="customer"/>
</layout>
suggestionPickerField
的属性:
-
asyncSearchDelayMs
- 设置最后一次按键到执行异步搜索之前需要延迟的时间。
-
metaClass
- 设置链接到组件的MetaClass
,如果在不绑定数据组件的情况下使用该组件,比如,没有设置 dataContainer 和 property。
-
minSearchStringLength
- 设置执行建议搜索需要的最小字符串长度。
-
popupWidth
- 设置建议选项弹出框的宽度。可选项:
-
auto
- 弹出框的宽度等于建议选项的最大长度,即自动宽度, -
parent
- 弹出框的宽度等于主组件的宽度, -
绝对(比如
"170px"
)或相对(比如"50%"
)值。
-
-
suggestionsLimit
- 设置显示的建议选项的最大数量。
suggestionPickerField
的元素:
-
actions
- 可选元素,用于描述与组件相关的各种操作,除了自定义的操作,suggestionPickerField
支持下列标准 PickerField 操作 :picker_lookup
、picker_clear
、picker_open
。
- SuggestionPickerField 的基本用法
-
一般情况下,给组件设置
SearchExecutor
就可以了。SearchExecutor
是一个包含单个方法的功能接口:List<E extends Entity> search(String searchString, Map<String, Object> searchParams)
:suggestionPickerField.setSearchExecutor((searchString, searchParams) -> { return Arrays.asList(entity1, entity2, ...); });
search()
方法在后台线程中执行,因此它无法访问可视化组件或可视化组件使用的数据源。可直接调用 DataManager 或中间件服务;或处理并返回预先加载到界面的数据。searchString
参数可用于使用用户输入的字符串过滤候选值。也可以使用escapeForLike()
方法来搜索包含特殊符号的值:suggestionPickerField.setSearchExecutor((searchString, searchParams) -> { searchString = QueryUtils.escapeForLike(searchString); return dataManager.loadList(LoadContext.create(Customer.class).setQuery( LoadContext.createQuery("select c from sample$Customer c where c.name like :name order by c.name escape '\\'") .setParameter("name", "%" + searchString + "%"))); });
- ParametrizedSearchExecutor 的用法
-
在前面的例子中,
searchParams
是一个空的字典。要定义参数,应该使用ParametrizedSearchExecutor
:suggestionPickerField.setSearchExecutor(new SuggestionField.ParametrizedSearchExecutor<Customer>(){ @Override public Map<String, Object> getParams() { return ParamsMap.of(...); } @Override public List<Customer> search(String searchString, Map<String, Object> searchParams) { return executeSearch(searchString, searchParams); } });
- EnterActionHandler 和 ArrowDownActionHandler 的用法
-
使用该组件的另一种方法是设置
EnterActionHandler
或ArrowDownActionHandler
。在建议弹出框隐藏的情况下,户按下 Enter 或 Arrow Down 键时会触发这些监听器。它们也是具有单一方法和单个参数 -currentSearchString
的功能接口。可以设置自己的操作处理器并使用SuggestionField.showSuggestions()
方法,该方法接收一个实体列表参数用于显示建议选项。suggestionPickerField.setArrowDownActionHandler(currentSearchString -> { List<Customer> suggestions = findSuggestions(); suggestionPickerField.showSuggestions(suggestions); }); suggestionPickerField.setEnterActionHandler(currentSearchString -> { List<Customer> suggestions = getDefaultSuggestions(); suggestionPickerField.showSuggestions(suggestions); });
- suggestionPickerField 的属性
-
align - asyncSearchDelayMs - caption - captionAsHtml - captionProperty - colspan - contextHelpText - contextHelpTextHtmlEnabled - css - dataContainer - datasource - description - descriptionAsHtml - editable - enable - box.expandRatio - height - icon - id - inputPrompt - metaClass - minSearchStringLength - popupWidth - property - required - requiredMessage - responsive - rowspan - stylename - suggestionsLimit - tabIndex - visible - width
- suggestionPickerField 的元素
- suggestionPickerField 的预定义样式
- API
-
addValueChangeListener - setContextHelpIconClickHandler - setOptionsStyleProvider
3.5.2.1.44. 表格
Table
组件以表格的方式展示信息,对数据进行排序 、管理表格列和表头,并对选中的行执行操作。
组件对应的 XML 名称: table
在界面 XML 描述中定义组件的示例:
<data readOnly="true">
<collection id="ordersDc" class="com.company.sales.entity.Order" view="order-with-customer">
<loader id="ordersDl">
<query>
<![CDATA[select e from sales_Order e]]>
</query>
</loader>
</collection>
</data>
<layout>
<table id="ordersTable" dataContainer="ordersDc" width="100%">
<columns>
<column id="date"/>
<column id="amount"/>
<column id="customer"/>
</columns>
<rowsCount/>
</table>
</layout>
table
元素:
-
rows
– 如果使用 datasource 属性来做数据绑定,则必须设置此元素。每行可以在左侧的附加列中有一个图标。在界面控制器中创建
ListComponent.IconProvider
接口的实现,并将其设置给表格:@Inject private Table<Customer> table; @Subscribe protected void onInit(InitEvent event) { table.setIconProvider(new ListComponent.IconProvider<Customer>() { @Nullable @Override public String getItemIcon(Customer entity) { CustomerGrade grade = entity.getGrade(); switch (grade) { case PREMIUM: return "icons/premium_grade.png"; case HIGH: return "icons/high_grade.png"; case MEDIUM: return "icons/medium_grade.png"; default: return null; } } }); }
-
columns
– 定义表格列的必须元素。每个列都在嵌套的
column
元素中描述,column
元素具有以下属性:-
id
− 必须属性,包含列中要显示的实体属性的名称。可以是来自数据容器的实体的属性,也可以是关联实体的属性(使用 "." 来指定属性在对象关系图中的路径)。例如:<columns> <column id="date"/> <column id="customer"/> <column id="customer.name"/> <column id="customer.address.country"/> </columns>
-
collapsed
− 可选属性;当设置为true
时默认隐藏列。当表格的columnControlVisible
属性不是false
时,用户可以通过表格右上角的菜单中的按钮 控制列的可见性。默认情况下,collapsed
是false
。
-
width
− 可选属性,控制默认列宽。只能是以像素为单位的数值。
-
align
− 可选属性,用于设置单元格的文本对齐方式。可选值:LEFT
、RIGHT
、CENTER
。默认为LEFT
。
-
editable
− 可选属性,允许编辑表中的相应列。为了使列可编辑,整个表的 editable 属性也应设置为true
。不支持在运行时更改此属性。
-
sortable
− 可选属性,用于禁用列的排序。整个表的 sortable 属性为true
此属性有效(默认为true
)。
-
maxTextLength
– 可选属性,允许限制单元格中的字符数。如果实际值和最大允许字符数之间的差异不超过 10 个字符,则多出来的字符不会被隐藏。用户可以点击可见部分来查看完整的文本。例如一列的字符数限制为 10 个字符:
-
link
- 如果设置为true
,则允许列中显示指向实体编辑器的链接。对于基本类型的列,link
属性也可以设置为 true; 在这种情况下,将打开主实体编辑器。这个方法可用于简化导航:用户能够通过单击一些关键属性快速地打开实体编辑器。
-
linkScreen
- 设置单击link
属性为true
的列中的链接时打开的界面的标识符。
-
linkScreenOpenType
- 设置界面打开模式(THIS_TAB
、NEW_TAB
或者DIALOG
)。
-
linkInvoke
- 单击链接时调用控制器方法而不是打开界面。@Inject private Notifications notifications; public void linkedMethod(Entity item, String columnId) { Customer customer = (Customer) item; notifications.create() .withCaption(customer.getName()) .show(); }
-
可选的
generator
属性包含指向界面控制器中方法,该方法可创建一个可视化组件显示在表格单元格中:<columns> <column id="name"/> <column id="imageFile" generator="generateImageFileCell"/> </columns>
public Component generateImageFileCell(Employee entity){ Image image = uiComponents.create(Image.NAME); image.setSource(FileDescriptorResource.class).setFileDescriptor(entity.getImageFile()); return image; }
它可以用来为 addGeneratedColumn() 方法提供一个
Table.ColumnGenerator
的实现 -
column
元素可能包含一个嵌套的formatter元素,它允许以不同于Datatype的标准格式显示属性值:<column id="date"> <formatter class="com.haulmont.cuba.gui.components.formatters.DateFormatter" format="yyyy-MM-dd HH:mm:ss" useUserTimezone="true"/> </column>
-
-
rowsCount
− 可选元素,为表格添加RowsCount
组件;此组件能够分页加载表格数据。可以使用数据加载器的setMaxResults()
方法限制数据容器中的记录数来定义分页的大小。这个方法通常是由链接到表格数据加载器的过滤器组件来执行的。如果表格没有通用过滤器,则可以直接从界面控制器调用此方法。RowsCount
组件还可以显示当前数据容器查询的记录总数,而无需提取这些记录。当用户单击 ? 图标时,它会调用com.haulmont.cuba.core.global.DataManager#getCount
方法,执行与当前查询条件相同的数据库查询,不过会使用COUNT(*)
聚合函数代替查询列。然后显示检索到的数字,代替 ? 图标。
table
属性:
-
multiselect
属性可以为表格行设置多选模式。如果multiselect
为true
,用户可以按住 Ctrl 或 Shift 键在表格中选择多行。默认情况下关闭多选模式。
-
sortable
属性可以对表中的数据进行排序。默认情况下,它设置为true
。如果允许排序,单击列标题在列名称右侧将显示图标 / 。可以使用sortable属性禁用特定列的排序。根据是否将所有记录放在了一页上来使用不同的方式进行排序。如果所有记录在一页,则在内存中执行排序而不需要数据库查询。如果数据有多页,则通过发送具有相应
ORDER BY
条件的新的查询请求在数据库中执行排序。一个表格中的列可能包含本地属性或实体链接。例如:
<table id="ordersTable" dataContainer="ordersDc"> <columns> <column id="customer.name"/> <!-- the 'name' attribute of the 'Customer' entity --> <column id="contract"/> <!-- the 'Contract' entity --> </columns> </table>
在后一种情况下,数据排序是根据关联实体的
@NamePattern
注解中定义的属性进行的。如果实体中没有这个注解,则仅仅在内存中对当前页的数据进行排序。如果列引用了非持久化实体属性,则数据排序将根据
@MetaProperty
注解的related()
参数中定义的属性执行。如果未指定相关属性,则仅仅在内存中对当前页的数据进行排序。如果表格链接到一个嵌套的属性容器,这个属性容器包含相关实体的集合。这个集合属性必须是有序类型(
List
或LinkedHashSet
)才能使表格支持排序。如果属性的类型为Set
,则sortable
属性不起作用,并且用户无法对表格进行排序。
-
presentations
属性控制展示设置。默认情况下,该值为false
。如果属性值为true
,则会在表格的右上角添加相应的图标 。
-
如果
columnControlVisible
属性设置为false
,则用户无法使用位于表头的右侧的下拉菜单按钮 隐藏列: 按钮位于表头的右侧。当前显示的列在菜单中标记为选中状态。
-
如果
reorderingAllowed
属性设置为false
,则用户不能通过用鼠标拖动来更改列顺序。
-
如果
columnHeaderVisible
属性设置为false
,则该表没有列标题。
-
如果
showSelection
属性设置为false
,则不突出显示当前行。
-
contextMenuEnabled
属性启用右键菜单。默认情况下,此属性设置为true
。右键菜单中会列出表格操作(如果有的话)和 System Information 菜单项(如果用户具有cuba.gui.showInfo
权限),通过 System Information 菜单项可查看选中实体的详细信息。
-
将
multiLineCells
设置为true
可以让包含多行文本的单元格显示多行文本。在这种模式下,浏览器会一次加载表格中当前页的所有行,而不是延迟加载表格的可见部分。这就要求在 Web 客户端中适当的滚动。默认值为“false”。
-
aggregatable
属性启用表格行的聚合运算。支持以下操作:-
SUM
– 计算总和 -
AVG
– 计算平均值 -
COUNT
– 计算总数 -
MIN
– 找到最小值 -
MAX
– 找到最大值
聚合列的
aggregation
元素应该设置type
属性,在这个属性中设置聚合函数。默认情况下,聚合列仅支持数值类型,例如Integer 、 Double 、 Long
和BigDecimal
。聚合表格值显示在表格顶部的附加行中。这是一个定义聚合表示例:<table id="itemsTable" aggregatable="true" dataContainer="itemsDc"> <columns> <column id="product"/> <column id="quantity"/> <column id="amount"> <aggregation type="SUM"/> </column> </columns> </table>
aggregation
元素还可以包含strategyClass
属性,指定一个实现AggregationStrategy
接口的类(参阅下面以编程方式设置聚合策略的示例)。<column id="amount"> <aggregation type="SUM"> <formatter class="com.company.sample.MyFormatter"/> </aggregation> </column>
aggregationStyle
属性允许指定聚合行的位置:TOP
或BOTTOM
。默认情况下使用TOP
。除了上面列出的操作之外,还可以自定义聚合策略,通过实现
AggregationStrategy
接口并将其传递给AggregationInfo
实例中Table.Column
类的setAggregation()
方法。例如:public class TimeEntryAggregation implements AggregationStrategy<List<TimeEntry>, String> { @Override public String aggregate(Collection<List<TimeEntry>> propertyValues) { HoursAndMinutes total = new HoursAndMinutes(); for (List<TimeEntry> list : propertyValues) { for (TimeEntry timeEntry : list) { total.add(HoursAndMinutes.fromTimeEntry(timeEntry)); } } return StringFormatHelper.getTotalDayAggregationString(total); } @Override public Class<String> getResultClass() { return String.class; } }
AggregationInfo info = new AggregationInfo(); info.setPropertyPath(metaPropertyPath); info.setStrategy(new TimeEntryAggregation()); Table.Column column = weeklyReportsTable.getColumn(columnId); column.setAggregation(info);
-
-
editable
属性可以将表格转换为即时编辑模式。在这种模式下,具有editable = true
属性的列显示用于编辑相应实体属性的组件。根据相应实体属性的类型自动选择每个可编辑列的组件类型。例如,对于字符串和数字属性,应用程序将使用 TextField;对于
Date
将使用 DateField;对于列表将使用 LookupField;对于指向其它实体的链接将使用 PickerField。对于
Date
类型的可编辑列,还可以定义dateFormat
或resolution
属性,类似于为 DateField 的属性。可以为显示链接实体的可编辑列定义 optionsContainer 和 captionProperty 属性。如果设置了
optionsContainer
属性,应用程序将使用 LookupField 而不是 PickerField。可以使用
Table.addGeneratedColumn()
方法实现单元格的自定义配置(包括编辑) - 见下文。
-
在具有基于 Halo-based 主题的 Web 客户端中,
stylename
属性可以在 XML 描述中或者界面控制器中为Table
组件设置预定义样式:<table id="table" dataContainer="itemsDc" stylename="no-stripes"> <columns> <column id="product"/> <column id="quantity"/> </columns> </table>
当以编程方式设置样式时,需要选择
HaloTheme
类的一个以TABLE_
为前缀的常量:table.setStyleName(HaloTheme.TABLE_NO_STRIPES);
表格样式:
-
borderless
- 不显示表格的外部边线。
-
compact
- 减少表格单元格内的空白区域。
-
no-header
- 隐藏表格的列标题。
-
no-horizontal-lines
- 删除行之间的水平分隔线。
-
no-stripes
- 删除交替的行颜色。
-
no-vertical-lines
- 删除列之间的垂直分隔线。
-
small
- 使用小字体并减少表格单元格内的空白区域。
-
Table
接口的方法:
-
可以使用
addColumnCollapsedListener
方法和ColumnCollapsedListener
接口的实现跟踪列的可视化状态。
-
getSelected()
、getSingleSelected()
返回表格中的选定行对应的实体实例。可以通过调用getSelected()
方法来获得集合。如果未选择任何内容,则程序将返回空集。如果禁用了multiselect
,应该使用getSingleSelected()
方法返回一个选定实体,如果没有选择任何内容则返回null
。
-
addSelectionListener()
可以跟踪表格选中行的变化,示例:customersTable.addSelectionListener(customerSelectionEvent -> notifications.create() .withCaption("You selected " + customerSelectionEvent.getSelected().size() + " customers") .show());
也可以通过订阅相应的事件来跟踪选中行的变化:
@Subscribe("customersTable") protected void onCustomersTableSelection(Table.SelectionEvent<Customer> event) { notifications.create() .withCaption("You selected " + customerSelectionEvent.getSelected().size() + " customers") .show(); }
可以使用isUserOriginated() 方法跟踪
SelectionEvent
事件的来源。
-
addGeneratedColumn()
方法允许在列中自定义数据的表现方式。它需要两个参数:列的标识符和Table.ColumnGenerator
接口的实现。如果标识符可以匹配 XML 描述中为表格列设置的标识符 - 在这种情况下,插入新列代替 XML 中定义的列。如果标识符与任何列都不匹配,则会在右侧添加新列。对于表的每一行将调用
Table.ColumnGenerator
接口的generateCell()
方法。该方法接受在相应行中显示的实体实例作为参数。generateCell()
方法应该返回一个可视化组件,该组件将显示在单元格中。使用组件的示例:
@Inject private GroupTable<Car> carsTable; @Inject private CollectionContainer<Car> carsDc; @Inject private CollectionContainer<Color> colorsDc; @Inject private UiComponents uiComponents; @Inject private Actions actions; @Subscribe protected void onInit(InitEvent event) { carsTable.addGeneratedColumn("color", entity -> { LookupPickerField<Color> field = uiComponents.create(LookupPickerField.NAME); field.setValueSource(new ContainerValueSource<>(carsDc, "color")); field.setOptions(new ContainerOptions<>(colorsDc)); field.addAction(actions.create(LookupAction.class)); field.addAction(actions.create(OpenAction.class)); return field; }); }
在上面的示例中,表中
color
列中的所有单元格都显示了 LookupPickerField 组件。组件应将它的值保存到相应的行中的实体的color
属性中。如果要显示动态文本,请使用特殊类
Table.PlainTextCell
而不是 Label 组件。它将简化渲染过程并使表格运行更快。如果
addGeneratedColumn()
方法接收到的参数是未在 XML 描述中声明的列的标识符,则新列的标题将设置如下:carsTable.getColumn("colour").setCaption("Colour");
还可以考虑使用 XML 的 generator 属性做更具声明性的设置方案。
-
requestFocus()
方法允许将焦点设置在某一行的具体的可编辑字段上。需要两个参数:表示行的实体实例和列的标识符。请求焦点的示例如下:table.requestFocus(item, "count");
-
scrollTo()
方法允许将表格滚动到具体行。需要一个参数:表示行的实体实例。滚动条的示例:
table.scrollTo(item);
-
如果需要在单元格中显示自定义内容并且在用户单击单元格的时候能收到通知,可以使用
setClickListener()
方法实现这些功能。CellClickListener
接口的实现接收选中实体和列标识符作为参数。这些单元格的内容将被包装在一个 span 元素中,这个 span 元素带有cuba-table-clickable-cell
样式,可以利用该样式来定义单元格外观。使用
CellClickListener
的示例:@Inject private Table<Customer> customersTable; @Inject private Notifications notifications; @Subscribe protected void onInit(InitEvent event) { customersTable.setCellClickListener("name", customerCellClickEvent -> notifications.create() .withCaption(customerCellClickEvent.getItem().getName()) .show()); }
-
setStyleProvider()
方法可以设置表格单元格显示样式。该方法接受Table.StyleProvider
接口的实现类作为参数。表格的每一行和每个单元分别调用这个接口的getStyleName()
方法。如果某一行调用该方法,则第一个参数包含该行显示的实体实例,第二个参数为null
。如果单元格调用该方法,则第二个参数包含单元格显示的属性的名称。设置样式的示例:
@Inject protected Table customersTable; @Subscribe protected void onInit(InitEvent event) { customersTable.setStyleProvider((customer, property) -> { if (property == null) { // style for row if (hasComplaints(customer)) { return "unsatisfied-customer"; } } else if (property.equals("grade")) { // style for column "grade" switch (customer.getGrade()) { case PREMIUM: return "premium-grade"; case HIGH: return "high-grade"; case MEDIUM: return "medium-grade"; default: return null; } } return null; }); }
然后应该在应用程序主题中设置的单元格和行样式。有关创建主题的详细信息,请参阅 主题。对于 Web 客户端,新样式在
styles.scss
文件中。在控制器中定义的样式名称,以及表格行和列的前缀标识符构成 CSS 选择器。例如:.v-table-row.unsatisfied-customer { font-weight: bold; } .v-table-cell-content.premium-grade { background-color: red; } .v-table-cell-content.high-grade { background-color: green; } .v-table-cell-content.medium-grade { background-color: blue; }
-
addPrintable()
当通过excel
标准操作或直接使用ExcelExporter
类导出数据到 XLS 文件时,此方法可以给列中数据设置自定义展现。该方法接收的两个参数为列标识符和为列提供的Table.Printable
接口实现。例如:ordersTable.addPrintable("customer", new Table.Printable<Customer, String>() { @Override public String getValue(Customer customer) { return "Name: " + customer.getName; } });
Table.Printable
接口的getValue()
方法应该返回在表格单元格中显示的数据。返回的数据不一定是字符串类型,该方法可以返回其它类型的值,比如数字或日期,它们将在 XLS 文件中以相应的类型展示。如果生成的列需要在输出到 XLS 时带有格式,则应该使用
addGeneratedColumn()
方法,传递一个Table.PrintableColumnGenerator
接口的实现作为参数。XLS 文档中单元格的值在这个接口的getValue()
方法中定义:ordersTable.addGeneratedColumn("product", new Table.PrintableColumnGenerator<Order, String>() { @Override public Component generateCell(Order entity) { Label label = uiComponents.create(Label.NAME); Product product = order.getProduct(); label.setValue(product.getName() + ", " + product.getCost()); return label; } @Override public String getValue(Order entity) { Product product = order.getProduct(); return product.getName() + ", " + product.getCost(); } });
如果没有以某种方式为生成的列定义
Printable
描述,那么该列将显示相应实体属性的值,如果没有关联的实体属性,则不显示任何内容。
-
setItemClickAction()
方法能够定义一个双击表格行时将执行的操作。如果未定义此操作,表格将尝试按以下顺序在其操作列表中查找适当的操作:-
由
shortcut
属性指定给 Enter 键的操作 -
edit
操作 -
view
操作如果找到此操作,并且操作具有
enabled=true
属性,则执行该操作。
-
-
setEnterPressAction()
方法可以定义按下 Enter 键时执行的操作。如果未定义此操作,则表将尝试按以下顺序在其操作列表中查找适当的操作:-
由
setItemClickAction()
方法定义的动作 -
由
shortcut
属性指定给 Enter 键的操作 -
edit
操作 -
view
操作
如果找到此操作,并且操作具有
enabled=true
属性,则执行该操作。 -
- table 的属性
-
align - aggregatable - aggregationStyle - caption - captionAsHtml - columnControlVisible - columnHeaderVisible - contextHelpText - contextHelpTextHtmlEnabled - contextMenuEnabled - css - dataContainer - description - descriptionAsHtml - editable - enable - box.expandRatio - height - id - multiLineCells - multiselect - presentations - reorderingAllowed - settingsEnabled - showSelection - sortable - stylename - tabIndex - textSelectionEnabled - visible - width
- table 的元素
-
actions - buttonsPanel - columns - rows - rowsCount
- column 元素的属性
-
align - caption - captionProperty - collapsed - dateFormat - editable - generator - id - link - linkInvoke - linkScreen - linkScreenOpenType - maxTextLength - optionsDatasource - resolution - sortable - visible - width
- column的元素
- aggregation的属性
- rows的属性
- table 的预定义样式
-
borderless - compact - no-header - no-horizontal-lines - no-stripes - no-vertical-lines - small
- API
-
addGeneratedColumn - addPrintable - addColumnCollapseListener - addSelectionListener - applySettings - generateCell - getSelected - requestFocus - saveSettings - scrollTo - setClickListener - setEnterPressAction - setItemClickAction - setStyleProvider
3.5.2.1.45. 文本区
TextArea
是多行文本编辑字段。
该组件对应的 XML 名称: textArea
TextArea
的功能大部分与 TextField 组件相同,同时具有以下特有属性:
-
cols
和rows
设置文本的行数和列数:<textArea id="textArea" cols="20" rows="5" caption="msg://name"/>
width
和height
的值优先于cols
和rows
的值。
-
wordwrap
- 将此属性设置为false
以关闭自动换行。TextArea
支持在其父TextInputField
接口中定义的TextChangeListener
。文本变化事件在输入时按顺序异步处理,不会阻塞输入。textArea.addTextChangeListener(event -> { int length = event.getText().length(); textAreaLabel.setValue(length + " of " + textArea.getMaxLength()); });
-
textChangeEventMode
定义文本的变化被发送到服务器并触发服务端事件的方式。有 3 种预定义的事件模式:-
LAZY
(默认) - 文件输入暂停时触发事件。暂停时间可以通过setTextChangeTimeout()
或者textChangeTimeout 属性修改。即使用户在输入文本时没有发生暂停,也会在可能发生的ValueChangeEvent
之前强制触发文本更改事件。 -
TIMEOUT
- 超时后触发事件。如果在超时周期内进行了多次更改,则将周期内自最后一次更改后发生的更改发送到服务端。可以使用setTextChangeTimeout()
或者textChangeTimeout 属性设置超时时长。如果在超时期限之前发生
ValueChangeEvent
,则在它之前触发TextChangeEvent
,条件是文本内容自上一个TextChangeEvent
以来已经发生改变。 -
EAGER
- 对于文本内容的每次更改,都会立即触发事件,通常是由按键触发。请求是独立且一个接一个地顺序处理。文本变化事件以异步方式与服务器交互,因此可以在处理事件请求的同时继续输入。
-
-
textChangeTimeout
当textChangeEventMode 是LAZY
或TIMEOUT
时,定义编辑文本时暂停的时间或者超时时间。- TextArea 样式
-
Web Client 使用 Halo-based 主题时,在 XML 描述或者界面控制器中可以使用
stylename
属性给TextArea
组件设置预定义的样式:<textArea id="textArea" stylename="borderless"/>
如果使用编程的方式设置样式,可以选择一个前缀为
TEXTFIELD_
的HaloTheme
class 常量:textArea.setStyleName(HaloTheme.TEXTAREA_BORDERLESS);
-
align-center
- 使文本在文本区中居中显示。
-
align-right
- 使文本在文本区中居右显示。
-
borderless
- 移除文本区的边框和背景。
-
- textArea 的属性
-
align - caption - captionAsHtml - caseConversion - cols - contextHelpText - contextHelpTextHtmlEnabled - conversionErrorMessage - css - dataContainer - datasource - datatype - description - descriptionAsHtml - editable - enable - box.expandRatio - height - icon - id - inputPrompt - maxLength - property - required - requiredMessage - responsive - rows - settingsEnabled - stylename - tabIndex - textChangeEventMode - textChangeTimeout - trim - visible - width - wordwrap
- textArea 的预定义样式
-
align-center - align-right - borderless - huge - large - small - tiny
- API
-
addTextChangeListener - addValueChangeListener - addValidator - commit - discard - isModified - setContextHelpIconClickHandler
3.5.2.1.46. 文本控件
TextField
是用于文本编辑的控件。它可以用于处理实体属性,也可用于输入/显示任何文本信息。
该组件对应的 XML 名称:textField
-
从本地消息包中获取标题(caption)的文本控件示例:
<textField id="nameField" caption="msg://name"/>
下图展示了一个简单文本控件示例:
-
Web Client 使用 Halo-based 主题时,在 XML 描述或者界面控制器中可以使用
stylename
属性给文本框组件设置预定义的样式 :<textField id="textField" stylename="borderless"/>
如果使用编程的方式设置样式,可以选择一个前缀为
TEXTFIELD_
的HaloTheme
class 常量。textField.setStyleName(HaloTheme.TEXTFIELD_INLINE_ICON);
TextField 样式:
-
align-center
- 使文本在文本框中居中显示。
-
align-right
- 使文本在文本框中居右显示。
-
borderless
- 移除文本框的边框和背景。
-
inline-icon
- 使标题图标显示在文本框里面。
文本框支持自动大小写转换。
caseConvertion
属性包含下列取值:-
UPPER
- 转换为大写, -
LOWER
- 转换为小写, -
NONE
- 不转换(默认值)。用这个选项来支持在输入法连续输入(比如,在输入中文、日文、韩文的时候)
-
-
要创建连接数据的文本框,使用数据容器和 property 属性。
<data> <instance id="customerDc" class="com.company.sales.entity.Customer" view="_local"> <loader/> </instance> </data> <layout> <textField dataContainer="customerDc" property="name" caption="msg://name"/> </layout>
如上所示,界面描述中为实体
Customer
定义了数据容器customerDc
,并且Customer
实体有一个name
属性。文本控件通过 dataContainer 属性连接到数据容器;property 属性设置为要显示在控件中的实体属性的名称。
-
如果文本控件没有连接到任何实体属性 (即,未设置数据容器和属性名称),可以使用
datatype
属性设置数据类型,数据类型用来格式化控件值。datatype
属性值可以是应用程序元数据中注册的任何数据类型 – 见 数据类型接口。通常,TextField
使用下面的数据类型:-
decimal
-
double
-
int
-
long
如果该字段有
datatype
属性,并且用户输入了一个错误的值,则会显示默认的转换错误消息。下面是一个数据类型是
Integer
的文本控件示例。<textField id="integerField" datatype="int" caption="msg://integerFieldName"/>
如果用户输入的字符不能解析为整数,当该控件失去焦点时,应用程序将显示错误消息。
默认的消息是在主语言包中定义的,有这样的模板:
databinding.conversion.error.<type>
,比如:databinding.conversion.error.int = Must be Integer
-
-
可以在界面 XML 描述中声明式的定义自己的类型转换错误消息,使用
conversionErrorMessage
属性:<textField conversionErrorMessage="This field can work only with Integers" datatype="int"/>
或者在界面控制器中通过便层的方式创建:
textField.setConversionErrorMessage("This field can work only with Integers");
-
可以为文本控件分配一个验证器 - 实现了
Field.Validator
接口的类。在datatype
对输入的字符格式进行验证后,验证器进行进一步的验证。例如,要创建一个正整数输入控件,需要创建一个验证器类:public class PositiveIntegerValidator implements Field.Validator { @Override public void validate(Object value) throws ValidationException { Integer i = (Integer) value; if (i <= 0) throw new ValidationException("Value must be positive"); } }
同时设置它为数据类型是
int
的文本控件的验证器:<textField id="integerField" datatype="int"> <validator class="com.sample.sales.gui.PositiveIntegerValidator"/> </textField>
与数据类型的输入时检查不同,验证不是在控件失去焦点时执行,而是在调用控件的
validate()
方法之后执行。这意味着控件(和连接的实体属性)可能暂时包含不满足验证条件的值(上例中的非正数)。这应该不是问题,因为要验证的控件通常用在编辑界面中,它会在提交之前自动调用所有控件的验证。如果该控件不在编辑界面中,则应在控制器中显式调用该控件的validate()
方法。
-
TextField
支持其实现的TextInputField
接口中定义的TextChangeListener
。为了不阻塞用户输入,文本变化事件的处理是异步进行的。textField.addTextChangeListener(event -> { int length = event.getText().length(); textFieldLabel.setValue(length + " of " + textField.getMaxLength()); }); textField.setTextChangeEventMode(TextInputField.TextChangeEventMode.LAZY);
-
The
TextChangeEventMode
定义文本的变化被发送到服务器并触发服务端事件的方式。有 3 种预定义的事件模式:-
LAZY
(默认) - 文件输入暂停时触发事件。暂停时间可以通过setInputEventTimeout()
修改。即使用户在输入文本时没有发生暂停,也会在可能发生的ValueChangeEvent
之前强制触发文本更改事件。 -
TIMEOUT
- 超时后触发事件。如果在超时周期内进行了多次更改,则将周期内自最后一次更改后发生的更改发送到服务端。可以使用setInputEventTimeout()
设置超时时长。如果在超时期限之前发生
ValueChangeEvent
,则在它之前触发TextChangeEvent
,条件是文本内容自上一个TextChangeEvent
以来已经发生改变。 -
EAGER
- 对于文本内容的每次更改,都会立即触发TextChangeEvent
事件,通常是由按键触发。请求是独立且一个接一个地顺序处理。文本变化事件以异步方式与服务器交互,因此可以在处理事件请求的同时继续输入。
-
-
EnterPressListener
允许定义一个在 Enter 键按下时被执行的操作:textField.addEnterPressListener(enterPressEvent -> notifications.create() .withCaption("Enter pressed") .show());
-
ValueChangeListener
当用户完成文本输入时,即在按下 Enter 键或组件失去焦点时,将触发ValueChangeListener
。ValueChangeEvent
类型的事件对象被传递给监听器,它有以下方法:-
getPrevValue()
返回组件之前的值。 -
getValue()
返回组件的当前值。textField.addValueChangeListener(stringValueChangeEvent -> notifications.create() .withCaption("Before: " + stringValueChangeEvent.getPrevValue() + ". After: " + stringValueChangeEvent.getValue()) .show());
可以使用 isUserOriginated() 方法跟踪
ValueChangeEvent
的来源。
-
-
如果文本控件连接到实体属性(通过
datasource
和property
),并且实体属性具有定义在@Column
JPA 注解中的length
参数,那么TextField
将相应地限制输入文本的最大长度。如果文本控件未链接到属性,或者属性未定义
length
值,或者需要覆盖此值,则可以使用maxLength
属性限制输入文本的最大长度。值 "-1" 表示没有限制。 例如:<textField id="shortTextField" maxLength="10"/>
-
默认情况下,文本控件截去字符串的前后空格。即,如果用户输入 " aaa bbb ",则
getValue()
方法返回并保存到连接实体属性的控件值将为 "aaa bbb"。可以通过将trim
属性设置为false
来禁用空格去除。应该注意的是,去除空格仅在用户输入新值时起作用。如果连接属性的值中已包含空格,则将显示空格,直到用户编辑该值。
-
文本控件始终返回
null
而不是输入的空字符串。因此,启用trim
属性后,任何只包含空格的字符串都将转换为null
。 -
setCursorPosition()
方法可使控件获得焦点并将光标位置设置为指定索引,索引基于 0。
- textField 的属性
-
align - caption - captionAsHtml - caseConversion - contextHelpText - contextHelpTextHtmlEnabled - conversionErrorMessage - css - dataContainer - datasource - datatype - description - descriptionAsHtml - editable - enable - box.expandRatio - height - icon - id - inputPrompt - maxLength - property - required - requiredMessage - stylename - tabIndex - trim - visible - width
- textField 的元素
- textField 的预定义样式
-
align-center - align-right - borderless - huge - inline-icon - large - small - tiny
- API
-
addEnterPressListener - addTextChangeListener - addValueChangeListener - commit - discard - isModified - setContextHelpIconClickHandler
3.5.2.1.47. 时间组件
TimeField
是用来显示和输入时间的组件。
组件的 XML 名称: timeField
-
要创建关联数据的时间组件,应该使用数据容器和 property 属性:
<data> <instance id="orderDc" class="com.company.sales.entity.Order" view="_local"> <loader/> </instance> </data> <layout> <timeField dataContainer="orderDc" property="deliveryTime"/> </layout>
如同上面的示例,在界面描述中为实体
Order
定义了数据容器orderDc
,实体具有deliveryTime
属性。时间输入组件的dataContainer
属性包含到数据容器的连接,property
属性 – 设置要显示在时间字段中的实体属性名称。关联的实体属性类型应该是
java.util.Date
或java.sql.Time
类型。
-
如果该控件不与实体属性相关联(比如没有设置数据容器和属性名称),可以使用
datatype
属性设置数据类型。TimeField
使用如下数据类型:-
localTime
-
offsetTime
-
time
-
-
无论上面提到的属性如何设置,都可以通过
showSeconds
属性控制是否显示秒。默认情况下,如果时间格式中包含ss
,则显示秒。<timeField dataContainer="orderDc" property="createTs" showSeconds="true"/>
- timeField 的属性
-
align - caption - captionAsHtml - contextHelpText - contextHelpTextHtmlEnabled - css - dataContainer - datasource - datatype - description - descriptionAsHtml - editable - enable - box.expandRatio - height - icon - id - property - required - requiredMessage - showSeconds - stylename - tabIndex - timeFormat - visible - width
- timeField 的元素
- API
-
addValueChangeListener - commit - discard - isModified - setContextHelpIconClickHandler
3.5.2.1.48. 标签列表
TokenList
组件提供了一种使用列表的简单方式: 以水平或垂直的方式标签名称、使用下拉列表添加标签、使用每个标签旁边的按钮进行移除。
组件 XML 名称: tokenList
下面是一个界面 XML 描述中定义 TokenList
组件的示例:
<data>
<instance id="orderDc" class="com.company.sales.entity.Order" view="order-edit">
<loader/>
<collection id="productsDc" property="products"/>
</instance>
<collection id="allProductsDc" class="com.company.sales.entity.Product" view="_minimal">
<loader id="allProductsDl">
<query><![CDATA[select e from sales_Product e order by e.name]]></query>
</loader>
</collection>
</data>
<layout>
<tokenList id="productsList" dataContainer="orderDc" property="products" inline="true" width="500px">
<lookup optionsContainer="allProductsDc"/>
</tokenList>
</layout>
在上面的示例中,嵌套的 productsDc
数据容器包含了一个订单的产品集合,同时 allProductsDc
数据容器包含了数据库中所有可用产品。id 为 productsList
的 TokenList
组件展示 productsDc
数据容器的内容,同时可以通过从 allProductsDc
中添加实例来改变集合。
tokenList
属性:
-
position
– 设置下拉列表的位置。这个属性的可选值包括:TOP
、BOTTOM
。默认是:TOP
。
-
inline
属性定义包含选中条目的列表如何显示:vertically(垂直) 或 horizontally(水平)。true
表示水平对齐,false
– 垂直对齐。 水平对齐的示例:
-
simple
– 设置为true
时,将隐藏选择组件同时显示 Add 按钮。点击 Add 按钮将弹出一个选择界面,界面上有实体实例列表,其类型由数据容器定义。选择界面的标识符是根据PickerField
的标准查找操作的规则来确定的。单击 Clear 按钮将删除TokenList
组件数据源中的所有元素。
-
clearEnabled
- 当设置为false
, Clear 按钮将隐藏。
tokenList
元素:
-
lookup
− 值选择组件的描述。lookup
元素属性:-
lookup
属性使TokenList
组件可以使用一个实体查找界面来选择值。
-
inputPrompt
- 显示在查找字段中的文本提示信息。如果没有设置,查找字段将不显示任何提示内容。<tokenList id="linesList" dataContainer="orderItemsDс" property="items" width="320px"> <lookup optionsDatasource="allItemsDs" inputPrompt="Choose an item"/> </tokenList>
-
multiselect
- 如果这个属性设置为true
,true
将传递到查找界面的参数字典的MULTI_SELECT
键。这个标志用于设置界面为多选模式。这个标志定义在WindowParams
枚举中,因此在旧版本的界面中可以通过以下方式使用:@Override public void init(Map<String, Object> params) { if (WindowParams.MULTI_SELECT.getBool(getContext())) { usersTable.setMultiSelect(true); } }
-
tokenList
监听器:
-
ItemClickListener
允许跟踪tokenList
列表条目上的点击操作。 -
ValueChangeListener
可以跟踪tokenList
组件值的变化,像其它任何实现了Field
接口的组件一样。可以使用 isUserOriginated() 方法来跟踪ValueChangeEvent
的来源。
- tokenList 的属性
-
align - caption - captionAsHtml - captionProperty - clearEnabled - contextHelpText - contextHelpTextHtmlEnabled - css - dataContainer - datasource - description - descriptionAsHtml - editable - enable - box.expandRatio - height - icon - id - inline - position - simple - stylename - tabIndex - visible - width
- tokenList 的元素
- lookup 的属性
-
captionProperty - filterMode - inputPrompt - lookup - lookupScreen - multiselect - openType - optionsDatasource
- button 的属性
- API
-
addValueChangeListener - setContextHelpIconClickHandler - setItemClickListener
3.5.2.1.49. 树
Tree
组件用于将具有自引用关系的实体显示为树状层次结构。
组件的 XML 名称: tree
下面是一个在界面 XML 描述中定义 tree
组件的示例:
<data readOnly="true">
<collection id="departmentsDc" class="com.company.sales.entity.Department" view="department-view">
<loader id="departmentsDl">
<query>
<![CDATA[select e from sales_Department e]]>
</query>
</loader>
</collection>
</data>
<layout>
<tree id="departmentsTree" dataContainer="departmentsDc" hierarchyProperty="parentDept"/>
</layout>
这里,dataContainer 属性包含指向集合数据容器的引用,hierarchyProperty
属性定义了一个实体属性的名称,这个属性也指向同一个的实体(从而能形成树)。
使用 treechildren
元素的 captionProperty
属性指定要显示为树节点名称的实体属性的名称,如果这个属性没有定义,将默认显示实体的实例名称。
在 Tree
中进行选择:
-
multiselect
设置是否允许树节点多选。如果multiselect
设置为true
,用户可在按住 Ctrl 或 Shift 键的情况下使用键盘或鼠标选择多个节点。多选模式默认关闭。
-
selectionMode
- 设置行选择模式。有三种预定义的选择模式:-
SINGLE
- 单一记录选择模式。 -
MULTI
- 多选模式,跟在表格中多选类似。 -
NONE
- 禁止选择。
行选择事件可以通过
SelectionListener
监听器进行跟踪。选择事件的发起者可以通过isUserOriginated()方法跟踪。selectionMode
属性比废弃的multiselect
属性有更高的优先级。 -
setItemClickAction()
用于定义一个操作,双击树节点时执行。
每个树节点左边可以定义一个图标。在界面控制器中的 setIconProvider()
方法中创建一个 Function
接口的实现来设置图标:
@Inject
private Tree<Department> tree;
@Subscribe
protected void onInit(InitEvent event) {
tree.setIconProvider(department -> {
if (department.getParentDept() == null) {
return "icons/root.png";
}
return "icons/leaf.png";
});
}
对于之前的老界面,Tree
组件可以绑定到一个数据源而不是数据容器。这种情况下,需要定义嵌套的 treechildren
元素,这个元素需要包含指向在 datasource 属性定义的 hierarchicalDatasource
的引用。hierarchicalDatasource
的声明需要包含 hierarchyProperty
属性,此属性定义了实体属性的名称,这个属性也指向相同的实体。
- tree 的属性
-
caption - captionAsHtml - captionProperty - contextHelpText - contextHelpTextHtmlEnabled - css - dataContainer - description - descriptionAsHtml - enable - box.expandRatio - height - id - multiselect - stylename - tabIndex - visible - width
- tree 的元素
- treechildren 的属性
3.5.2.1.50. 树形数据网格
TreeDataGrid
, 类似于DataGrid组件,用于显示和排序表格式数据,并提供显示层级结构数据和操作行列的方法,这些行和列由于只在滚动时按需加载数据,因而性能更好。
该组件用于显示具自引用关系的实体。例如,它可以用于展示文件系统或公司组织结构图。
该组件对应的 XML 名称: treeDataGrid
对于 TreeDataGrid
,应该设置两个属性:dataContainer
,它将 treeDataGrid
绑定到数据容器;hierarchyProperty
,它是引用同一实体的实体属性的名称。
在界面 XML 描述中定义的组件示例:
<data readOnly="true">
<collection id="departmentsDc" class="com.company.sales.entity.Department" view="department-view">
<loader id="departmentsDl">
<query>
<![CDATA[select e from sales_Department e]]>
</query>
</loader>
</collection>
</data>
<layout>
<treeDataGrid id="treeDataGrid" dataContainer="departmentsDc" hierarchyProperty="parentDept">
<columns>
<column id="name" property="name"/>
<column id="parentDept" property="parentDept"/>
</columns>
</treeDataGrid>
</layout>
TreeDataGrid
的功能类似于简单的DataGrid。
- treeDataGrid 的属性
-
align - caption - captionAsHtml - colspan - columnResizeMode - columnsCollapsingAllowed - contextMenuEnabled - css - dataContainer - datasource - description - descriptionAsHtml - editorBuffered - editorCancelCaption - editorEnabled - editorSaveCaption - enable - box.expandRatio - frozenColumnCount - headerVisible - height - hierarchyProperty - icon - id - reorderingAllowed - responsive - rowspan - selectionMode - settingsEnabled - sortable - stylename - tabIndex - textSelectionEnabled - visible - width
- treeDataGrid 的元素
-
actions - buttonsPanel - columns - rowsCount
- column 的属性
-
caption - collapsed - collapsible - collapsingToggleCaption - editable - expandRatio - id - maximumWidth - minimumWidth - property - resizable - sortable - width
- column 的元素
- API
-
addGeneratedColumn - applySettings - createRenderer - edit - saveSettings - getColumns - setDescriptionProvider - setCellStyleProvider - setConverter - setDetailsGenerator - setEnterPressAction - setItemClickAction - setRenderer - setRowDescriptionProvider - setRowStyleProvider - sort
- 树形数据网格的监听器
-
ColumnCollapsingChangeListener - ColumnReorderListener - ColumnResizeListener - ContextClickListener - EditorCloseListener - EditorOpenListener - EditorPostCommitListener - EditorPreCommitListener - ItemClickListener - SelectionListener - SortListener
3.5.2.1.51. 树形表格
TreeTable
组件是在最左列显示一个"树结构"的具有层级关系的表格。这个组件用于具有自引用关系的实体。比如,文件系统或公司的组织结构图。
组件的 XML 名称: treeTable
TreeTable
组件的 dataContainer 属性应该包含指向集合数据容器的引用。hierarchyProperty
属性定义实体的一个属性,此属性也指向相同的实体。
下面是一个在界面 XML 描述中定义 treeTable
组件的示例:
<data readOnly="true">
<collection id="departmentsDc" class="com.company.sales.entity.Department" view="_local">
<loader id="departmentsDl">
<query>
<![CDATA[select e from sales_Department e]]>
</query>
</loader>
</collection>
</data>
<layout>
<treeTable id="departmentsTable" dataContainer="departmentsDc" hierarchyProperty="parentDept" width="100%">
<columns>
<column id="name"/>
<column id="active"/>
</columns>
</treeTable>
</layout>
TreeTable
的功能与简单的Table组件相似。
- treeTable 的属性
-
align - aggregatable - aggregationStyle - caption - captionAsHtml - columnControlVisible - contextHelpText - contextHelpTextHtmlEnabled - contextMenuEnabled - css - dataContainer - description - descriptionAsHtml - editable - enable - box.expandRatio - height - id - multiLineCells - multiselect - presentations - reorderingAllowed - settingsEnabled - sortable - stylename - tabIndex - textSelectionEnabled - visible - width
- treeTable 的元素
-
actions - buttonsPanel - columns - rows - rowsCount
- column 的元素
-
align - caption - captionProperty - collapsed - dateFormat - editable - id - link - linkInvoke - linkScreen - linkScreenOpenType - maxTextLength - optionsDatasource - resolution - sortable - visible - width
- column 的元素
- rows 的属性
- API
-
addColumnCollapseListener - addSelectionListener - setClickListener
3.5.2.1.52. 双列
twinColumn
是用于选择多个值的两个列表组件。左边的列表包含可选的未选择值列表,右边的列表是已经选择的值列表。用户通过在左右两个列表移动值来进行选择或取消选择值,移动操作可通过双击或点击按钮完成。每个值都可以定义自己的展示样式和图标。
组件的 XML 名称: twinColumn
下面是一个使用 twinColumn
组件选择实体实例的示例:
<data>
<instance id="orderDc" class="com.company.sales.entity.Order" view="order-edit">
<loader/>
<collection id="productsDc" property="products"/>
</instance>
<collection id="allProductsDc" class="com.company.sales.entity.Product" view="_minimal">
<loader>
<query>
<![CDATA[select e from sales_Product e]]>
</query>
</loader>
</collection>
</data>
<layout>
<twinColumn id="twinColumn"
dataContainer="productsDc"
property="name"
optionsContainer="allProductsDc"/>
</layout>
addAllBtnEnabled
属性用于配置组件是否显示在两列之间移动所有值的按钮。
columns
属性设置一行中可显示的字符数,rows
属性– 配置每个列表中的行数。
列表中每个条目的外观可通过实现 TwinColumn.StyleProvider
接口来定义,可以为列表中每个实体实例返回样式名和图标路径。
可以使用 CheckBoxGroup 组件中描述的 setOptionsList()
、 setOptionsMap()
和 setOptionsEnum()
方法任意指定组件的选项列表。
- twinColumn 组件的属性
-
align - addAllBtnEnabled - caption - captionAsHtml - captionProperty - columns - contextHelpText - contextHelpTextHtmlEnabled - css - dataContainer - datasource - description - descriptionAsHtml - editable - enable - box.expandRatio - height - icon - id - leftColumnCaption - optionsContainer - optionsDatasource - property - required - requiredMessage - rightColumnCaption - rows - stylename - tabIndex - visible - width
- TwinColumn 组件的元素
- API
3.5.2.2. 布局容器
3.5.2.2.1. 折叠布局
Accordion
是可折叠内容的容器,允许在隐藏和显示大量内容之间切换。
该组件的 XML 名称:accordion
界面 XML 描述中的折叠布局示例:
<accordion id="accordion" height="100%">
<tab id="tabStamford" caption="msg://tabStamford" margin="true" spacing="true">
<label value="msg://sampleStamford"/>
</tab>
<tab id="tabBoston" caption="msg://tabBoston" margin="true" spacing="true">
<label value="msg://sampleBoston"/>
</tab>
<tab id="tabLondon" caption="msg://tabLondon" margin="true" spacing="true">
<label value="msg://sampleLondon"/>
</tab>
</accordion>
accordion
组件应包含用于描述标签页的 tab
元素。每个标签页都是一个容器,具有类似于 vbox 的垂直组件布局。如果应用程序界面的空间有限或标签页的标题太长而无法显示在 TabSheet
中,则可以使用 Accordion 容器。Accordion 具有平滑切换的动态效果。
tab
元素的属性:
-
lazy
– 设置标签内容延迟加载。在打开界面时,延迟加载的标签页不会立即加载其内容,这样可以减少内存中的组件数量。仅当用户选择标签页时,才会加载标签页中的组件。此外,如果延迟标签页中包含的可视化组件带有连接到 JPQL 查询的数据源,则也不会执行此查询。因此,界面可以更快地打开,只有当用户选中标签页请求数据时才会加载数据。
请注意,刚打开界面时,延迟加载标签页上包含的组件并不存在。因此,这些组件不能被注入到控制器,也不能在控制器的
init()
方法中使用getComponent()
方法来获取。只有在用户打开延迟加载的标签页后,才能访问其中的组件。可以使用Accordion.SelectedTabChangeListener
处理标签页选中事件,例如:@Inject private Accordion accordion; private boolean tabInitialized; @Subscribe protected void onInit(InitEvent event) { accordion.addSelectedTabChangeListener(selectedTabChangeEvent -> { if ("tabCambridge".equals(selectedTabChangeEvent.getSelectedTab().getName())) { initCambridgeTab(); } }); } private void initCambridgeTab() { if (tabInitialized) { return; } tabInitialized = true; (1) }
1 初始化代码写在这。在这里使用 getComponentNN("comp_id")
获取延迟加载标签页上的组件。默认情况下,标签页是非延迟的,这表示当界面打开时标签页中的内容将立即被加载。
-
在使用了基于 Halo 主题的 Web 客户端,可以通过将
borderless
预定义样式应用到stylename
属性来移除accordion
组件的边框和背景。accordion.setStyleName(HaloTheme.ACCORDION_BORDERLESS);
accordion
标签页可以包含其它可视化组件,比如网格、表格等:
<accordion id="accordion" height="100%" width="100%" enable="true">
<tab id="tabNY" caption="msg://tabNY" margin="true" spacing="true">
<table id="nYTable" width="100%">
<columns>
<column id="borough"/>
<column id="county"/>
<column id="population"/>
<column id="square"/>
</columns>
<rows datasource="newYorkDs"/>
</table>
</tab>
</accordion>
- accordion 的属性
-
caption - captionAsHtml - colspan - contextHelpText - contextHelpTextHtmlEnabled - css - description - descriptionAsHtml - enable - box.expandRatio - height - id - rowspan - stylename - tabCaptionsAsHtml - tabIndex - visible - width
- tab 的属性
-
caption - description - enable - expand - icon - id - lazy - margin - spacing - stylename - visible
- API
-
add - addSelectedTabChangeListener - getComponent - getComponentNN - getComponents - getOwnComponent - getOwnComponents - remove - removeAll
3.5.2.2.2. 盒子布局
BoxLayout
是一个顺序排列组件的容器。
有三种类型的 BoxLayout
,它们对应的 XML 元素如下:
-
hbox
− 组件在水平方向顺序排列。<hbox spacing="true" margin="true"> <dateField dataContainer="orderDc" property="date"/> <lookupField dataContainer="orderDc" property="customer" optionsContainer="customersDc"/> <textField dataContainer="orderDc" property="amount"/> </hbox>
-
vbox
− 组件在垂直方向顺序排列。vbox
默认具有 100%的宽度。<vbox spacing="true" margin="true"> <dateField dataContainer="orderDc" property="date"/> <lookupField dataContainer="orderDc" property="customer" optionsContainer="customersDc"/> <textField dataContainer="orderDc" property="amount"/> </vbox>
-
flowBox
− 组件被水平排列在一行。如果一行中没有足够的空间,则排列不下的组件将显示在下一行中(行为类似于 Swing 的FlowLayout
)。<flowBox spacing="true" margin="true"> <dateField dataContainer="orderDc" property="date"/> <lookupField dataContainer="orderDc" property="customer" optionsContainer="customersDc"/> <textField dataContainer="orderDc" property="amount"/> </flowBox>
在基于 Halo 的主题的 Web 客户端中,BoxLayout
可用于创建更复杂的组合布局。 使用两个 Box 布局,一个 vbox 布局,设置 stylename
为 card
或 well
。里面嵌套一个 hbox 布局, 并为其设置属性 stylename="v-panel-caption"
, 使用这个方法可以定义一个具有标题的面板,看起来像 Vaadin Panel。
-
card
使布局看起来像卡片。 -
well
样式使卡片的外看起来带有下沉阴影效果。
<vbox stylename="well"
height="200px"
width="300px"
expand="message"
spacing="true">
<hbox stylename="v-panel-caption"
width="100%">
<label value="Widget caption"/>
<button align="MIDDLE_RIGHT"
icon="font-icon:EXPAND"
stylename="borderless-colored"/>
</hbox>
<textArea id="message"
inputPrompt="Enter your message here..."
width="280"
align="MIDDLE_CENTER"/>
<button caption="Send message"
width="100%"/>
</vbox>
getComponent()方法允许通过索引获取 BoxLayout
的子组件:
Button button = (Button) hbox.getComponent(0);
可以在 BoxLayout
中使用键盘快捷键。使用 addShortcutAction()
方法设置快捷方式和要执行的操作:
flowBox.addShortcutAction(new ShortcutAction("SHIFT-A", shortcutTriggeredEvent ->
notifications.create()
.withCaption("SHIFT-A action")
.show()
));
- hbox 、 vbox 、 flowBox 的属性
-
align - caption - captionAsHtml - contextHelpText - contextHelpTextHtmlEnabled - css - description - descriptionAsHtml - enable - expand - box.expandRatio - height - id - margin - spacing - stylename - visible - width
- API
-
add - addLayoutClickListener - addShortcutAction - getComponent - getComponentNN - getComponents - getMargin - getOwnComponent - getOwnComponents - indexOf - remove - removeAll - setMargin - setSpacing
3.5.2.2.3. 按钮面板
ButtonsPanel
是一个容器,它简化了表格上用于数据管理的组件(通常是按钮)的使用和排列。
该组件的 XML 名称:buttonsPanel
界面 XML 描述中定义 ButtonsPanel
的示例:
<table id="customersTable" dataContainer="customersDc" width="100%">
<actions>
<action id="create" type="create"/>
<action id="edit" type="edit"/>
<action id="remove" type="remove"/>
<action id="excel" type="excel"/>
</actions>
<columns>
<column id="name"/>
<column id="email"/>
</columns>
<rowsCount/>
<buttonsPanel id="buttonsPanel" alwaysVisible="true">
<button id="createBtn" action="customersTable.create"/>
<button id="editBtn" action="customersTable.edit"/>
<button id="removeBtn" action="customersTable.remove"/>
<button id="excelBtn" action="customersTable.excel"/>
</buttonsPanel>
</table>
buttonsPanel
元素可以位于 table
内,也可以位于界面的其它任何位置。
如果 buttonsPanel
位于 table
中,则它会与表格的 rowsCount 组件组合,从而可以在垂直方向上节省空间。此外,如果使用 Frame.openLookup()
方法打开查找界面(例如,从 PickerField 组件),这时按钮面板会变为隐藏状态。
使用 Frame.openLookup()
打开查找界面时,可以利用 alwaysVisible
属性禁止隐藏按钮面板。如果属性值为 true
,则不会隐藏按钮面板。默认情况下,属性值为 false
。
可以使用 LayoutClickListener
接口拦截对 buttonsPanel
区域的点击。
可以在 ButtonsPanel
中使用键盘快捷键。使用 addShortcutAction()
方法设置快捷方式和要执行的操作:
buttonsPanel.addShortcutAction(new ShortcutAction("SHIFT-A", shortcutTriggeredEvent ->
notifications.create()
.withCaption("SHIFT-A action")
.show()
));
- buttonsPanel 的属性
-
align - alwaysVisible - caption - captionAsHtml - contextHelpText - contextHelpTextHtmlEnabled - css - description - descriptionAsHtml - enable - expand - box.expandRatio - height - id - stylename - visible - width
- API
3.5.2.2.4. CSS 布局
CssLayout
是一个容器,可以使用 CSS 完全控制这个容器里的组件的位置和样式。
该组件的 XML 名称: cssLayout
。
下面是使用 cssLayout
实现简单地响应式界面的示例。
在宽屏中显示组件:
在窄屏中显示组件:
界面的 XML 描述:
<cssLayout responsive="true" stylename="responsive-container" width="100%">
<vbox margin="true" spacing="true" stylename="group-panel">
<textField caption="Field One" width="100%"/>
<textField caption="Field Two" width="100%"/>
<button caption="Button"/>
</vbox>
<vbox margin="true" spacing="true" stylename="group-panel">
<textField caption="Field Three" width="100%"/>
<textField caption="Field Four" width="100%"/>
<button caption="Button"/>
</vbox>
</cssLayout>
modules/web/themes/halo/halo-ext.scss
文件的内容 (参考 扩展现有主题 创建这个文件):
/* Define your theme modifications inside next mixin */
@mixin halo-ext {
@include halo;
.responsive-container {
&[width-range~="0-900px"] {
.group-panel {
width: 100% !important;
}
}
&[width-range~="901px-"] {
.group-panel {
width: 50% !important;
}
}
}
}
-
stylename
属性允许在 XML 描述或界面控制器中为CssLayout
组件设置预定义样式。-
v-component-group
样式用于创建组件分组,即一行无缝连接的组件:<cssLayout stylename="v-component-group"> <textField inputPrompt="Search..."/> <button caption="OK"/> </cssLayout>
-
well
样式使窗口的外看起来带有下沉阴影效果。 -
card
样式使布局看起来像卡片。与嵌套的具有属性stylename="v-panel-caption"
的布局组合使用,可以创建复杂的组合布局,例如:<cssLayout height="300px" stylename="card" width="300px"> <hbox stylename="v-panel-caption" width="100%"> <label value="Widget caption"/> <button align="MIDDLE_RIGHT" icon="font-icon:EXPAND" stylename="borderless-colored"/> </hbox> <vbox height="100%"> <label value="Panel content"/> </vbox> </cssLayout>
效果如下:
-
- cssLayout 的属性
-
caption - captionAsHtml - contextHelpText - contextHelpTextHtmlEnabled - css - description - descriptionAsHtml - enable - box.expandRatio - height - id - responsive - stylename - visible - width
- API
-
add - addShortcutAction - addLayoutClickListener - getComponent - getComponentNN - getComponents - getOwnComponent - getOwnComponents - indexOf - remove - removeAll
3.5.2.2.5. 框架
frame
元素用于在界面中引入框架。
属性:
-
src
− 指向一个框架 XML 描述的路径。
-
screen
– 框架在screens.xml 中的标识符。(如果框架已注册)。
应定义其中一个属性。如果定义了两个属性,则根据 src
显式设置的文件加载框架。
- frame 的属性
-
align - caption - captionAsHtml - css - description - descriptionAsHtml - height - box.expandRatio - id - screen - src - stylename - visible - width
- API
-
add - getComponent - getComponentNN - getComponents - getMargin - getOwnComponent - getOwnComponents - indexOf - remove - removeAll - setMargin - setSpacing
3.5.2.2.6. 网格布局
GridLayout
容器将组件放置到网格中。
该组件的 XML 名称:grid
使用示例:
<grid spacing="true">
<columns count="4"/>
<rows>
<row>
<label value="Date" align="MIDDLE_LEFT"/>
<dateField dataContainer="orderDc" property="date"/>
<label value="Customer" align="MIDDLE_LEFT"/>
<lookupField dataContainer="orderDc" property="customer" optionsContainer="customersDc"/>
</row>
<row>
<label value="Amount" align="MIDDLE_LEFT"/>
<textField dataContainer="orderDc" property="amount"/>
</row>
</rows>
</grid>
grid
元素:
-
columns
– 必须的元素,描述网格列。该元素需要有一个count
属性或嵌套的column
元素。在最简单的情况下,只须使用
count
属性设置列数即可。如果容器宽度以像素或百分比显式定义,则列宽度平均分配。要非均等地分配界面空间,应为每列定义具有
flex
属性的column
元素。网格示例,其中第二列和第四列占用所有剩余的水平空间,第四列占用的空间是第二列的三倍:
<grid spacing="true" width="100%"> <columns> <column/> <column flex="1"/> <column/> <column flex="3"/> </columns> <rows> <row> <label value="Date"/> <dateField dataContainer="orderDc" property="date" width="100%"/> <label value="Customer"/> <lookupField dataContainer="orderDc" property="customer" optionsContainer="customersDc" width="100%"/> </row> <row> <label value="Amount"/> <textField dataContainer="orderDc" property="amount" width="100%"/> </row> </rows> </grid>
如果未定义
flex
,或者设置为 0,则根据其内容设置列的宽度,此时需要至少有另一列设置了非零的flex
。在上面的示例中,第一列和第三列将根据最大文本长度设置宽度。如果需要展示剩余可用空间,整个容器宽度应设置为像素或百分比。否则,将根据内容长度计算列宽,并且
flex
属性不会起作用,也就看不到可用空间了。
-
rows
− 必须的元素,包含一组行。每一行都使用自己的row
元素定义。row
元素也可以有flex
属性,与column
的 flex 定义类似,影响具有给定总高度的网格的垂直可用空间的分布。
<grid spacing="true">
<columns count="4"/>
<rows>
<row>
<label value="Name 1"/>
<textField/>
<label value="Name 2"/>
<textField/>
</row>
<row>
<label value="Name 3"/>
<textField colspan="3" width="100%"/>
</row>
</rows>
</grid>
这时,组件会按以下方式放置:
可以使用 LayoutClickListener
接口拦截在 GridLayout
区域上的点击。
getComponent() 方法允许通过其列和行索引获取 GridLayout
的子组件:
Button button = (Button) gridLayout.getComponent(0,1);
可以在 GridLayout
中使用键盘快捷键。使用 addShortcutAction()
方法设置快捷方式和要执行的操作:
grid.addShortcutAction(new ShortcutAction("SHIFT-A", shortcutTriggeredEvent ->
notifications.create()
.withCaption("SHIFT-A action")
.show()
));
- grid 的属性
-
align - caption - captionAsHtml - contextHelpText - contextHelpTextHtmlEnabled - css - description - descriptionAsHtml - enable - box.expandRatio - height - id - margin - spacing - stylename - visible - width
- grid 的属性
- columns 的属性
- column 的属性
- row 的属性
- API
-
add - addShortcutAction - addLayoutClickListener - getComponent - getComponentNN - getComponents - getMargin - getOwnComponent - getOwnComponents - remove - removeAll - setMargin - setSpacing
3.5.2.2.7. 分组框布局
GroupBoxLayout
是一个容器,可以将一组组件框在一个区域并为它们设置一个整体的标题。另外,这个区域还可以折叠起来。
该组件的 XML 名称:groupBox
下面是一个分组框布局的 XML 描述示例:
<groupBox caption="Order">
<dateField dataContainer="orderDc" property="date" caption="Date"/>
<lookupField dataContainer="orderDc" property="customer" optionsContainer="customersDc" caption="Customer"/>
<textField dataContainer="orderDc" property="amount" caption="Amount"/>
</groupBox>
groupBox
的属性:
-
caption
– 分组标题。
-
orientation
– 定义组件放置的方向 - 水平或垂直。默认值为vertical
(垂直)。
-
collapsable
– 如果该值设置为true
,可以使用 / 按钮将组件的内容隐藏。
-
collapsed
– 如果设置为true
,初始状态下组件内容会被折叠。collapsed
属性在collapsable="true"
有效。下面是一个折叠的
GroupBox
的例子:可以通过
ExpandedStateChangeListener
接口获取groupBox
组件的展开状态改变事件。
-
outerMargin
- 设置groupBox
边框的外边距。如果设置为true
,组件的所有边都会添加外边距。要单独设置每一边的外边距,请为groupBox
的每一边设置true
或false
:<groupBox outerMargin="true, false, true, false">
如果
showAsPanel
属性设置为 true,则忽略outerMargin
属性。
-
showAsPanel
– 如果设置为true
,该组件看起来就会像 Vaadin Panel。默认值为false
。
默认情况下,groupBox
容器的宽是 100%,类似于vbox。
在基于 Halo 主题的 Web 客户端中,可以使用 XML 描述或界面控制器中的 stylename
属性为 groupBox
组件设置预定义样式。以编程方式设置样式时,选择一个以 LAYOUT_
或 GROUPBOX_
为前缀的 HaloTheme 类常量。showAsPanel
属性设置为 true
时可以与以下样式结合使用:
-
borderless
样式删除groupBox
的边框和背景颜色:groupBox.setShowAsPanel(true); groupBox.setStyleName(HaloTheme.GROUPBOX_PANEL_BORDERLESS);
-
card
样式会使布局看起来像卡片布局。
-
well
样式会使容器具有下沉阴影效果:<groupBox caption="Well-styled groupBox" showAsPanel="true" stylename="well" width="300px" height="200px"/>
可以在 Groupbox
中使用快捷键。使用 addShortcutAction()
方法设置快捷方式和要执行的操作:
groupBox.addShortcutAction(new ShortcutAction("SHIFT-A", shortcutTriggeredEvent ->
notifications.create()
.withCaption("SHIFT-A action")
.show()
));
- groupBox 的属性
-
align - caption - captionAsHtml - collapsable - collapsed - contextHelpText - contextHelpTextHtmlEnabled - css - description - descriptionAsHtml - expand - box.expandRatio - height - id - orientation - outerMargin - settingsEnabled - showAsPanel - spacing - stylename - width
- groupBox 的预定义样式
-
borderless - card - well
- API
-
add - addExpandedStateChangeListener - addShortcutAction - applySettings - getComponent - getComponentNN - getComponents - getOwnComponent - getOwnComponents - indexOf - remove - removeAll - saveSettings - setOuterMargin - setSpacing
3.5.2.2.8. HTML 盒子布局
HtmlBoxLayout
是一个可以在 HTML 模板中定义组件位置的容器。布局模板包含在一个主题中。
不要将 |
该组件的 XML 名称:htmlBox
下面是一个使用 htmlBox
的简单界面的例子。
界面的 XML 描述:
<htmlBox align="TOP_CENTER"
template="sample"
width="500px">
<label id="logo"
value="Subscribe"
stylename="logo"/>
<textField id="email"
width="100%"
inputPrompt="email@test.test"/>
<button id="submit"
width="100%"
invoke="showMessage"
caption="Subscribe"/>
</htmlBox>
htmlBox
的属性:
-
template
属性定义了一个位于主题的layouts
子目录中的 HTML 文件的名称。在创建模板之前,应该创建主题扩展或自定义主题。例如,如果使用 Halo 主题并且
template
属性是my_template
, 那么模板文件应该是modules/web/themes/halo/layouts/my_template.html
。HTML 模板的内容在
modules/web/themes/halo/layouts/sample.html
文件中:<div location="logo" class="logo"></div> <table class="component-container"> <tr> <td> <div location="email" class="email"></div> </td> <td> <div location="submit" class="submit"></div> </td> </tr> </table>
模板应包含带有
location
属性的<div>
元素。这些元素将显示 XML 描述中定义的有相应标识符的 CUBA 组件。modules/web/themes/halo/com.company.application/halo-ext.scss
文件的内容如下(要创建文件请参阅 扩展现有主题 ):@mixin com_company_application-halo-ext { .email { width: 390px; } .submit { width: 100px; } .logo { font-size: 96px; text-transform: uppercase; margin-top: 50px; } .component-container { display: inline-block; vertical-align: top; width: 100%; } }
-
templateContents
属性设置了模板的内容,用于直接定义布局。例如:
<htmlBox height="256px" width="400px"> <templateContents> <![CDATA[ <table align="center" cellspacing="10" style="width: 100%; height: 100%; color: #fff; padding: 20px; background: #31629E repeat-x"> <tr> <td colspan="2"><h1 style="margin-top: 0;">Login</h1> <td> </tr> <tr> <td align="right">User name:</td> <td> <div location="username"></div> </td> </tr> <tr> <td align="right">Password:</td> <td> <div location="password"></div> </td> </tr> <tr> <td align="right" colspan="2"> <div location="okbutton" style="padding: 10px;"></div> </td> </tr> <tr> <td colspan="2" style="padding: 7px; background-color: #4172AE"><span style="font-family: FontAwesome; margin-right: 5px;"></span> This information is in the layout. <td> </tr> </table> ]]> </templateContents> <textField id="username" width="100%"/> <textField id="password" width="100%"/> <button id="okbutton" caption="Login"/> </htmlBox>
- htmlBox 的属性
-
align - caption - captionAsHtml - contextHelpText - contextHelpTextHtmlEnabled - css - description - descriptionAsHtml - enable - box.expandRatio - height - id - stylename - template - templateContents - visible - width
- API
-
add - getComponent - getComponentNN - getComponents - getOwnComponent - getOwnComponents - remove - removeAll
3.5.2.2.9. layout
layout
的属性:
-
maxHeight
- 设置窗口布局最大的 CSS 高度,比如"640px"
、"100%"
。
-
minHeight
- 设置窗口布局最小的 CSS 高度,比如"640px"
、"100%"
。
-
maxWidth
- 设置窗口布局最大的 CSS 宽度,比如"640px"
、"100%"
。
-
minWidth
- 设置窗口布局最小的 CSS 宽度,比如"640px"
、"100%"
。
示例:
<layout minWidth="600px"
minHeight="200px">
<textArea width="800px"/>
</layout>
这些属性在弹出对话框中也有效:
<dialogMode forceDialog="true"
width="500"
height="250"/>
<layout minWidth="600px"
minHeight="200px">
<textArea width="250px"/>
</layout>
3.5.2.2.10. 滚动盒子布局
ScrollBoxLayout
− 一个支持内容滚动的容器。
该组件的 XML 名称: scrollBox
下面是一个 XML 描述示例:
<groupBox caption="Order" width="300" height="170">
<scrollBox width="100%" height="100%" spacing="true" margin="true">
<dateField dataContainer="orderDc" property="date" caption="Date"/>
<lookupField dataContainer="orderDc" property="customer" optionsContainer="customersDc" caption="Customer"/>
<textField dataContainer="orderDc" property="amount" caption="Amount"/>
</scrollBox>
</groupBox>
-
组件排列的方向可以通过
orientation
属性定义 ,可选值:horizontal
或vertical
。默认为vertical
。
-
scrollBars
属性可以配置滚动条。它的值可以是horizontal
或者vertical
- 分别用于水平滚动和垂直滚动,both
- 两个方向都有滚动条。将值设置为none
禁止向任何方向的滚动。
-
contentHeight
- 设置内容高度。
-
contentWidth
- 设置内容宽度。
-
contentMaxHeight
- 设置内容的最大 CSS 高度,例如,"640px"
、"100%"
。
-
contentMinHeight
- 设置内容的最小 CSS 高度,例如,"640px"
、"auto"
。
-
contentMaxWidth
- 设置内容的最大 CSS 宽度,例如,"640px"
、"100%"
。
-
contentMinWidth
- 设置内容的最小 CSS 宽度,例如,"640px"
、"auto"
。
<layout>
<scrollBox contentMinWidth="600px"
contentMinHeight="200px"
height="100%"
width="100%">
<textArea height="150px"
width="800px"/>
</scrollBox>
</layout>
建议设置内容的高和宽,否则,放置在 如果没有设置内容高和宽,不要给嵌套组件设置 |
可以在 ScrollBox
中使用快捷键。使用 addShortcutAction()
方法设置快捷键和要执行的操作:
scrollBox.addShortcutAction(new ShortcutAction("SHIFT-A", shortcutTriggeredEvent ->
notifications.create()
.withCaption("SHIFT-A action")
.show()
));
- scrollBox 的属性
-
align - caption - captionAsHtml - contextHelpText - contextHelpTextHtmlEnabled - css - description - descriptionAsHtml - box.expandRatio - height - id - margin - orientation - scrollBars - spacing - stylename - width
- API
-
add - addShortcutAction - getComponent - getComponentNN - getComponents - getMargin - getOwnComponent - getOwnComponents - indexOf - remove - removeAll - setMargin - setSpacing
3.5.2.2.11. 分隔面板
SplitPanel
− 由可移动的分隔条分隔成两个区域的容器。
该组件的 XML 名称:split
下面是一个分隔面板的 XML 描述示例:
<split orientation="horizontal" pos="30" width="100%" height="100%">
<vbox margin="true" spacing="true">
<dateField dataContainer="orderDc" property="date" caption="Date"/>
<lookupField dataContainer="orderDc" property="customer" optionsContainer="customersDc" caption="Customer"/>
</vbox>
<vbox margin="true" spacing="true">
<textField dataContainer="orderDc" property="amount" caption="Amount"/>
</vbox>
</split>
split
容器必须包含两个嵌套的容器或组件。它们将显示在分隔条的两侧。
split
的属性:
-
dockable
- 启用或禁用SplitPanel
停靠按钮,默认值为false
。停靠功能仅适用于水平方向的
SplitPanel
。
-
dockMode
- 定义停靠方向。可以使用LEFT
或RIGHT
作为值。<split orientation="horizontal" dockable="true" dockMode="RIGHT"> ... </split>
-
minSplitPosition
、maxSplitPosition
- 可以通过像素或百分比来定义分割条的可用位置范围。如下所示,可以限制分隔条从组件左侧移动 100 到 300 像素:
<split id="splitPanel" maxSplitPosition="300px" minSplitPosition="100px" width="100%" height="100%"> <vbox margin="true" spacing="true"> <button caption="Button 1"/> <button caption="Button 2"/> </vbox> <vbox margin="true" spacing="true"> <button caption="Button 4"/> <button caption="Button 5"/> </vbox> </split>
如果想以编程方式来设置范围,请使用
Component.UNITS_PIXELS
或Component.UNITS_PERCENTAGE
来指定值的单位:splitPanel.setMinSplitPosition(100, Component.UNITS_PIXELS); splitPanel.setMaxSplitPosition(300, Component.UNITS_PIXELS);
-
orientation
– 定义组件方向。horizontal
- 嵌套的组件水平放置,vertical
- 嵌套的组件垂直放置。
-
pos
– 整数,定义第一个组件区域与第二个组件区域的百分比。例如,pos="30"
表示区域比例为 30/70。默认情况下,区域比例为 50/50。
-
reversePosition
- 定义从组件的另一侧指定分隔条的pos
属性。
-
如果
locked
属性设置为true
,则用户无法更改分隔条位置。
-
带有
large
值的stylename
属性会使分隔条变地宽一点。split.setStyleName(HaloTheme.SPLITPANEL_LARGE);
SplitPanel
的方法:
-
可以使用
getSplitPosition()
方法来获取分隔条的位置。
-
可以使用
PositionUpdateListener()
方法来获取分隔条移动事件。可以使用 isUserOriginated() 方法来跟踪SplitPositionChangeEvent
的来源。 -
如果需要获取分隔条位置的单位,请使用
getSplitPositionUnit()
方法。返回值为Component.UNITS_PIXELS
或Component.UNITS_PERCENTAGE
。 -
如果从组件的另一侧设置位置,那么
isSplitPositionReversed()
方法会返回true
。
- split 的属性
-
align - caption - captionAsHtml - contextHelpText - contextHelpTextHtmlEnabled - css - description - descriptionAsHtml - dockable - dockMode - box.expandRatio - height - id - locked - minSplitPosition - maxSplitPosition - orientation - pos - reversePosition - settingsEnabled - stylename - width
- API
-
add - addPositionUpdateListener - applySettings - getComponent - getComponentNN - getComponents - getOwnComponent - getOwnComponents - remove - removeAll - saveSettings
3.5.2.2.12. 标签页面板
TabSheet
容器是一个标签页面板。该面板一次只显示一个标签页的内容。
该组件的 XML 名称:tabSheet
下面是一个标签页面板的 XML 描述示例:
<tabSheet>
<tab id="mainTab" caption="Tab1" margin="true" spacing="true">
<dateField dataContainer="orderDc" property="date" caption="Date"/>
<lookupField dataContainer="orderDc" property="customer" optionsContainer="customersDc" caption="Customer"/>
</tab>
<tab id="additionalTab" caption="Tab2" margin="true" spacing="true">
<textField dataContainer="orderDc" property="amount" caption="Amount"/>
</tab>
</tabSheet>
tabSheet 的 description 属性定义了一个提示信息,当用户将光标悬停在标签页区域上或单击标签页区域时,提示信息会显示在弹出窗口中。
tabSheet
组件应该包含 tab
元素来描述标签页。每个标签页都是一个具有类似于 vbox 的垂直组件布局的容器。
tab
元素属性:
-
id
– 标签页标识符。请注意,标签页不是组件,它们的 ID 仅在TabSheet
中使用,以便在控制器中引用标签页。 -
caption – 标签页标题。
-
description - 提示文本,当用户将光标悬停在具体标签页上或单击具体标签页时,提示文本会在弹出窗口中显示。
-
lazy
– 设置标签页内容延迟加载。当界面打开时,延迟标签页不会加载其内容,这样可以减少内存中的组件数量。只有当用户选择某个标签页时,才会加载标签页中的组件。另外,如果延迟标签页包含连接到使用 JPQL 查询的数据源的可视化组件,也不会立即执行 JPQL 查询。因此,界面会打开得更快,并且只有当用户通过选择标签页请求数据时才会加载其中的数据。
请注意,当界面打开时,在延迟标签页上的组件是不存在的。因此,它们不能注入到控制器中,并且不能通过调用控制器的
init()
方法中getComponent()
方法来获得。只有在用户打开标签页后才能访问延迟标签页中的组件。可以使用TabSheet.SelectedTabChangeListener
拦截这个操作,例如:@Inject private TabSheet tabSheet; private boolean detailsInitialized, historyInitialized; @Subscribe protected void onInit(InitEvent event) { tabSheet.addSelectedTabChangeListener(selectedTabChangeEvent -> { if ("detailsTab".equals(selectedTabChangeEvent.getSelectedTab().getName())) { initDetails(); } else if ("historyTab".equals(selectedTabChangeEvent.getSelectedTab().getName())) { initHistory(); } }); } private void initDetails() { if (detailsInitialized) { return; } detailsInitialized = true; (1) } private void initHistory() { if (historyInitialized) { return; } historyInitialized = true; (2) }
1 在这里使用 getComponentNN("comp_id")
方法获取标签上的组件2 在这里使用 getComponentNN("comp_id")
方法获取标签上的组件默认情况下,标签页不是
lazy
延迟加载,在界面打开时就会加载所有内容。可以使用 isUserOriginated() 方法来跟踪
SelectedTabChangeEvent
事件的来源。- 标签页布局样式
-
在具有 Halo-based 主题的 Web 客户端中,可以使用 XML 描述或界面控制器中的
stylename
属性为TabSheet
容器设置预定义样式:<tabSheet stylename="framed"> <tab id="mainTab" caption="Framed tab"/> </tabSheet>
当以编程方式设置样式时,请使用
HaloTheme
类中的以TABSHEET_
为前缀的常量:tabSheet.setStyleName(HaloTheme.TABSHEET_COMPACT_TABBAR);
-
centered-tabs
- 使得标签页在标签栏内居中。如果所有标签页完全适合标签栏(即没有标签栏滚动),效果最佳。
-
compact-tabbar
- 减少标签栏中标签页周围的空白。
-
equal-width-tabs
- 为标签栏中的所有标签页提供相等的宽度(即所有标签页的展开比例都为 1)。如果标签页标题不适合标签页会被缩短。应用此样式时标签页滚动将会被禁用(所有标签页将同时显示)。
-
framed
- 在整个组件周围以及标签页栏中的各个标签页周围添加边框。
-
icons-on-top
- 在标签页标题上显示标签页图标(默认情况下,图标位于标题的左侧)。
-
only-selected-closeable
- 只有选中的标签页显示关闭按钮。不会阻止以编程方式关闭标签页,它仅仅是对用户隐藏了关闭按钮。
-
padded-tabbar
- 为标签栏中的标签页周围添加少量内边距,以便它们不会紧挨着组件的外边缘。
-
- tabSheet 的属性
-
caption - captionAsHtml - contextHelpText - contextHelpTextHtmlEnabled - css - description - descriptionAsHtml - box.expandRatio - height - id - stylename - tabCaptionsAsHtml - tabIndex - tabsVisible - visible - width
- tab 的属性
-
caption - closable - description - enable - expand - margin - icon - id - lazy - spacing - stylename - visible
- tabSheet 的预定义样式
-
centered-tabs - compact-tabbar - equal-width-tabs - framed - framed - only-selected-closeable - padded-tabbar
- API
-
add - addSelectedTabChangeListener - getComponent - getComponentNN - getComponents - getOwnComponent - getOwnComponents - remove - removeAll
3.5.2.3. 界面布局规则
下面的章节介绍如何在界面上正确放置可视化组件和容器。
3.5.2.3.1. 组件位置
- 适应内容的尺寸
- 固定大小
-
固定大小表示组件的尺寸在运行时不会改变。
XML<vbox width=”320px” height=”240px”/>
Javavbox.setWidth(”320px”);
- 相对大小
-
相对大小表示组件将按可用空间百分比来占用空间。
XML<label width=”100%”/>
Javalabel.setWidth(”50%”);
使用相对尺寸的组件会响应可用空间大小的变化,在界面上调整其实际大小。
- 容器特性
-
例如:
<layout> <button caption="Button"/> <button caption="Button"/> </layout>
默认情况下,组件和容器的宽度和高度取决于其中的内容。一些容器有不同的默认尺寸:
容器 宽 高 100%
AUTO
100%
AUTO
100%
AUTO
根 layout 元素是一个垂直布局的容器(
VBox
),它的宽度和高度都是 100%。弹窗模式下的高度可以是AUTO
。TabSheet 中的标签页是 VBox 容器。
GroupBox
组件包含VBox
或HBox
,具体取决于其 orientation 属性值。自适应大小的容器示例:
<layout> <vbox> <button caption="Button"/> <button caption="Button"/> </vbox> </layout>
具有相对尺寸的容器示例:
<layout spacing="true"> <groupBox caption="GroupBox" height="100%"/> <button caption="Button"/> </layout>
这里,
layout
,以及vbox
( 或hbox
),为所有内部嵌套组件提供相等的空间,groupBox
的高度为 100%。除此之外,groupBox
的宽度默认为 100%并占用所有可用空间。
- 组件特性
- 扩展(expand)选项
-
容器的 expand 属性用来指定会被赋于最大可用空间的组件。
指定为
expand
的组件在组件扩展方向上(对于VBox
是垂直方向,对于HBox
是水平方向)会占用其容器的所有剩余空间。更改容器大小时,这种组件会相应地调整自身大小。<vbox expand="bigBox"> <vbox id="bigBox"/> <label value="Label"/> </vbox>
expand
在对组件的扩展上也只是相对有效,例如,下面示例中宽度固定的 groupBox 不能横向扩展:<layout spacing="true" expand="groupBox"> <groupBox id="groupBox" caption="GroupBox" width="200px"/> <button caption="Button"/> </layout>
在下面示例中,使用了一个起辅助作用的 Label(spacer)元素。由于将其指定为
expand
,所以这个空标签占用了容器中剩余的所有空间。<layout expand="spacer"> <textField caption="Number"/> <dateField caption="Date"/> <label id="spacer"/> <hbox spacing="true"> <button caption="OK"/> <button caption="Cancel"/> </hbox> </layout>
3.5.2.3.2. 外边距和间距
- 界面边框的外边距
-
margin 属性允许在容器边框和嵌套组件之间设置边距。
如果
margin
设置为true
,则容器所有的边都会有边距。<layout> <vbox margin="true" height="100%"> <groupBox caption="Group" height="100%"/> </vbox> <groupBox caption="Group" height="100%"/> </layout>
也可以单独为每个的边(上、右、下、左)设置边距。为顶部和底部启用外边距的示例:
<vbox margin="true,false,true,false">
- 组件之间的间距
-
spacing 属性表明是否应在容器扩展方向上的嵌套组件之间添加间距。
在某些嵌套组件变得不可见的情况下,间距也会正常工作,所以不要使用
margin
来模拟间距。<layout spacing="true"> <button caption="Button"/> <button caption="Button"/> <button caption="Button"/> <button caption="Button"/> </layout>
3.5.2.3.3. 对齐
- 容器内的组件对齐
-
使用 align 属性来对齐容器内的组件。
比如,下面的示例中标签(label)位于容器的中心:
<vbox height="100%"> <label align="MIDDLE_CENTER" value="Label"/> </vbox>
指定了对齐方式的组件在对齐方向上不应设置 100%的大小。容器会提供比组件所需空间更多的空间。组件将在此空间内对齐。
在可用空间内的对齐示例:
<layout> <groupBox height="100%" caption="Group"/> <label align="MIDDLE_CENTER" value="Label"/> </layout>
3.5.2.3.4. 常见的布局错误
- 常见错误 1. 为自适应尺寸(根据内容)的容器内的组件设置相对尺寸
-
具有相对尺寸的错误布局示例:
在此示例中,
label
具有 100%的高度,而VBox
的默认高度是 AUTO,即基于内容自适应。使用 expand 的错误布局示例:
Expand 隐式将标签设置为 100%的相对高度,与上面的示例一样,这种做法不正确。 在这种情况下,界面可能看起来不像预期的那样。某些组件可能会消失或大小为零。如果遇到一些奇怪的布局问题,请首先检查是否正确指定了相对尺寸。
- 常见错误 2. 给 ScrollBox 中的组件指定了 100%的尺寸
-
错误布局示例:
由于这样的错误,即使嵌套组件的大小超过滚动区域,
ScrollBox
中的滚动条也不会出现。
- 常见错误 3. 没有足够空间情况下的组件对齐
-
错误布局的示例:
在此示例中,
HBox
根据内容自适应大小,因此标签对齐无效。
3.5.2.4. 其它
本章介绍了跟可视化组件相关的一些通用组件。
3.5.2.4.1. UiComponents
UiComponents
是个工厂类,可以使用该类按名称、类或者类型标识创建 UI 组件。
如果创建关联数据的组件,使用类型标识执行特定值类型来对组件进行参数化。比如对于 Label
,TextField
或者 DateField
组件,使用类型标识 TextField.TYPE_INTEGER
。当创建关联到实体的组件,比如 PickerField
,LookupField
或者 Table
,使用静态的 of()
方法来获取合适的类型标识。对于其它组件和容器,使用组件类作为参数。
示例:
@Inject
private UiComponents uiComponents;
@Subscribe
protected void onInit(InitEvent event) {
// components working with simple data types
Label<String> label = uiComponents.create(Label.TYPE_STRING);
TextField<Integer> amountField = uiComponents.create(TextField.TYPE_INTEGER);
LookupField<String> stringLookupField = uiComponents.create(LookupField.TYPE_STRING);
// components working with entities
LookupField<Customer> customerLookupField = uiComponents.create(LookupField.of(Customer.class));
PickerField<Customer> pickerField = uiComponents.create(PickerField.of(Customer.class));
Table<OrderLine> table = uiComponents.create(Table.of(OrderLine.class));
// other components and containers
Button okButton = uiComponents.create(Button.class);
VBoxLayout vBox = uiComponents.create(VBoxLayout.class);
// ...
}
3.5.2.4.2. 格式化控件
格式化控件只能跟只读组件一起用,比如Label,Table Column等等。对于可编辑的组件值,比如TextField 应该用 Datatype 机制来格式化。 |
在界面的 XML 描述中,组件的格式化控件可以在嵌套的 formatter
元素中定义。这个元素有一个单一的属性:
-
class
− 实现了com.haulmont.cuba.gui.components.Formatter
接口的一个类。
如果格式化控件的构造函数中有 org.dom4j.Element
的参数,那么这个格式化控件可以接受额外的属性来描述此格式化控件。比如,可以用格式化的字符串作为这个额外的参数。CUBA 框架里的 DateFormatter
和 NumberFormatter
这两个类就可以从 format
属性读取格式化模板:
<column id="date">
<formatter class="com.haulmont.cuba.gui.components.formatters.DateFormatter" format="yyyy-MM-dd HH:mm:ss"/>
</column>
另外,DateFormatter
类也能识别 type
属性,这个属性可以有 DATE
和 DATETIME
两个值。如果用了这个属性,其实就是使用 Datatype 机制的 dateFormat
或者 dateTimeFormat
来做格式化,比如:
<column id="endDate">
<formatter class="com.haulmont.cuba.gui.components.formatters.DateFormatter" type="DATE"/>
</column>
默认情况下,DateFormatter
用服务器的时区来显示日期和时间。如果需要使用用户的时区来显示,设置格式化控件的 useUserTimezone
属性为 true
。
如果格式化控件对应的类是一个内部类,那么这个类需要声明成
|
格式化控件除了可以通过 XML 描述来分配给组件之外,也可以通过编程的方式实现 - 在组件的 setFormatter()
方法里设置一个格式化类的实例。
下面这个例子是声明一个自定义的格式化控件类,然后在表格的某一列里面使用:
public class CurrencyFormatter implements Formatter<BigDecimal> {
protected GeneralConfiguration generalConfiguration;
protected Currency currentCurrency;
public CurrencyFormatter(GeneralConfiguration generalConfiguration) {
this.generalConfiguration = generalConfiguration;
currentCurrency = generalConfiguration.getCurrency();
}
@Override
public String format(BigDecimal value) {
return currentCurrency.format(value);
}
}
protected void initTableColumns() {
Formatter<BigDecimal> currencyFormatter = new CurrencyFormatter(generalConfiguration);
table.getColumn("totalPrice").setFormatter(currencyFormatter);
}
3.5.2.4.3. 展示设置
展示设置机制允许用户管理表格(table)的列宽、排序方式等外观属性。
用户通过展示设置可以:
-
使用唯一的名称保存展示设置。表格的设置会自动保存在当前活动的展示设置中。
-
编辑和删除展示设置。
-
在不同设置之间切换。
-
指定一个默认展示设置,当界面打开的时候会用这个设置来显示表格。
-
创建全局的展示设置,对所有用户可见。如果需要创建、修改或者删除全局展示设置,此用户需要有
cuba.gui.presentations.global
安全权限。
实现了 com.haulmont.cuba.gui.components.Component.HasPresentations
接口的组件都可以使用展示设置。这些组件是:
3.5.2.4.4. 定时器
定时器是一个非可见的界面组件,可以以一定的时间间隔运行一些界面控制器的代码。定时器是在一个处理用户事件的线程里面运行的,所以可以更新界面组件。当创建定时器的界面被关闭之后,定时器就会停止工作了。
该组件对应的 XML 名称:timer
在 Web 客户端,定时器是基于浏览器的轮询(polling)机制实现,在 desktop 客户端,是基于 javax.swing.Timer
实现的。
创建定时器的主要方法是在界面的 XML 描述中进行声明 - 使用 timers
元素,位置在 dsContext
和 layout
元素之间。
-
delay
- 必选属性;按毫秒定义定时器执行的时间间隔。 -
autostart
– 可选属性;当设置成true
的时候,定时器会在界面打开的时候立即自动启动。默认值是false
,也就是说只有在调用定时器的start()
方法之后才会启动。 -
repeating
– 可选属性;开启定时器的重复执行模式。如果这个属性设置的是true
,定时器会按照delay
设置的时间间隔反复一轮一轮的执行。否则只会在delay
设定的毫秒时间之后执行一次。 -
onTimer
– 可选属性;定义一个当定时器触发的时候需要执行的方法名称。这个对应的方法必须定义在界面控制器中,需要是public
的,而且有一个com.haulmont.cuba.gui.components.Timer
类型的参数。
下面这个例子演示了使用定时器来周期性的刷新表格内容:
<window ...
<dsContext>
<collectionDatasource id="bookInstanceDs" ...
</dsContext>
<timers>
<timer delay="3000" autostart="true" repeating="true" onTimer="refreshData"/>
</timers>
<layout ...
@Inject
private CollectionDatasource bookInstanceDs;
public void refreshData(Timer timer) {
bookInstanceDs.refresh();
}
定时器可以注入一个界面控制器,也可以通过 Window.getTimer()
方法获得。定时器的执行可以用定时器的 start()
和 stop()
方法控制。对于已经启动的定时器,会忽略再次调用 start()
,但是当定时器使用 stop()
方法停止之后,可以通过 start()
方法再次启动。
下面这个例子演示了在 XML 中定义定时器,然后在控制器里面使用定时器监听:
<timers>
<timer id="helloTimer" delay="5000"/>
</timers>
@Inject
private Timer helloTimer;
@Inject
private Notifications notifications;
@Subscribe("helloTimer")
protected void onHelloTimerTimerAction(Timer.TimerActionEvent event) { (1)
notifications.create()
.withCaption("Hello")
.show();
}
@Subscribe("helloTimer")
protected void onHelloTimerTimerStop(Timer.TimerStopEvent event) { (2)
notifications.create()
.withCaption("Timer is stopped")
.show();
}
@Subscribe
protected void onInit(InitEvent event) { (3)
helloTimer.start();
}
1 | 定时器执行处理器 |
2 | 定时器停止事件 |
3 | 启动定时器 |
定时器也可以在控制器里面创建,如果是这样的话,需要显式的把这个定时器加到界面中,比如:
@Inject
private Notifications notifications;
@Inject
private UiComponents uiComponents;
@Subscribe
protected void onInit(InitEvent event) {
Timer helloTimer = uiComponents.create(Timer.NAME);
getWindow().addTimer(helloTimer); (1)
helloTimer.setId("helloTimer"); (2)
helloTimer.setDelay(5000);
helloTimer.setRepeating(true);
helloTimer.addTimerActionListener(e -> { (3)
notifications.create()
.withCaption("Hello")
.show();
});
helloTimer.addTimerStopListener(e -> { (4)
notifications.create()
.withCaption("Timer is stopped")
.show();
});
helloTimer.start(); (5)
}
1 | 在页面中添加定时器 |
2 | 设置定时器参数 |
3 | 添加执行处理器 |
4 | 添加停止事件监听器 |
5 | 启动定时器 |
3.5.2.4.5. 验证器控件
Validator
设计用来检查可视化组件中输入的值。
验证和输入检查是需要区分开来的,输入检查是说:假设一个文本组件(比如,TextField)的数据类型设置的不是字符串(这种情况可能出现在绑定实体属性或者手动设置控件的 验证不会在输入同时或者失去焦点时马上反馈,而是会在组件的 |
在界面的 XML 描述中,组件的验证器可以在嵌套的 validator
元素中定义。validator
元素可以有这些属性:
-
script
− Groovy 脚本的路径。这个脚本用来做验证。 -
class
− 实现了Field.Validator
接口的 Java 类名。
Groovy 验证脚本和标准 Java 验证器类,位置在 com.haulmont.cuba.gui.components.validators
包,支持 message
属性,用来定义验证失败时显示的消息。这个属性的值可以是包含消息内容的字符串或者一个当前界面语言消息包内的一个消息键名。比如:
<validator class="com.haulmont.cuba.gui.components.validators.PatternValidator"
message="msg://validationError"
pattern="\d{3}"/>
# messages.properties
validationError = Input error
可以通过 CUBA Studio 添加验证器。下面是给 fieldGroup
组件添加验证器的例子:
验证机制是按如下方式确定的:
-
如果
script
属性的值没有设置,并且validator
元素也没有包含 Groovy 表达式的文本,系统会使用class
属性定义的类来进行验证。<field property="amount"> <validator class="com.haulmont.cuba.gui.components.validators.DoubleValidator"/> </field>
-
如果
validator
元素包含了文本,那这个文本会被当作 Groovy 表达式通过 Scripting 来执行。<field property="year"> <validator> value ==~ /\d+/ </validator> </field>
-
否则,系统会使用 Scripting 来运行定义在
script
里的 Groovy 脚本。<field property="zipCode"> <validator script="com.company.demo.web.address.ZipValidator"/> </field>
变量 value
会被传入 Groovy 表达式或者脚本,这个值包含了可视化组件中用户输入的值。表达式或者脚本需要返回一个 boolean
值: true
表示验证通过,false
表示没通过。
如果使用 Java 类作为验证器,这个类要么有一个不带任何参数的默认构造函数,要么有带有如下参数组合的构造函数:
-
org.dom4j.Element
,String
– 这个构造函数会接收验证器的 XML 元素和界面消息包的名字。 -
org.dom4j.Element
– 这个构造函数会接收验证器的 XML 元素。
如果验证器是用内部类的方式实现的,这个类需要是
|
平台已经包含了一组常用的验证器实现(参考 com.haulmont.cuba.gui.components.validators
包),可以在项目里使用:
-
DateValidator - 日期验证
-
DoubleValidator - 双浮点数验证
-
EmailValidator - 邮件地址验证
-
IntegerValidator - 整数类型验证
-
LongValidator - 长整型验证
-
PatternValidator - 正则模式验证
-
ScriptValidator - 脚本验证
-
StringValidator - 字符串类型验证
验证器不仅可以通过嵌套的 XML 配置给组件,也可以通过编程的方式 - 通过提交一个验证器实例给组件的 addValidator()
方法。
下面的例子就是以编程方式创建了一个邮编的验证器类:
public class ZipValidator implements Field.Validator {
@Override
public void validate(Object value) throws ValidationException {
if (value != null && ((String) value).length() != 6)
throw new ValidationException("Zip must be of 6 characters length");
}
}
下面例子演示在 FieldGroup 组件使用邮编验证器和标准的正则模式验证器:
<fieldGroup>
<field property="zip" required="true">
<validator class="com.company.sample.gui.ZipValidator"/>
</field>
<field property="imei">
<validator class="com.haulmont.cuba.gui.components.validators.PatternValidator"
pattern="\d{15}"
message="IMEI validation failed"/>
</field>
</fieldGroup>
这个是在界面控制器中以编程方式设置验证器:
if (Boolean.TRUE.equals(parameter.getRequired())) {
tokenList.addValidator(new Field.Validator() {
@Override
public void validate(Object value) throws ValidationException {
if (value instanceof Collection && CollectionUtils.isEmpty((Collection) value)) {
throw new ValidationException(getMessage("paramIsRequiredButEmpty"));
}
}
});
}
3.5.2.5. 可视化组件 API
- 通用
-
-
unwrap()
- 返回针对不同客户端的组件实例(Vaadin 或者 Swing 组件)。可以在 Client 模块简化底层 API 的调用,参考 使用 Vaadin 组件 章节。com.vaadin.ui.TextField vTextField = textField.unwrap(com.vaadin.ui.TextField.class);
-
unwrapComposition()
- 返回针对不同客户端的最外层的外部包裹容器(external container)。可以在 Client 模块简化底层 API 的调用。
这两个方法支持所有可视化组件。
-
- Buffered-缓冲写入模式接口
-
-
commit()
- 从上次更新之后的所有改动更新到数据源。
-
discard()
- 从上次更新之后所有的改动都废弃掉。对象会从数据源更新数据。
-
isModified()
- 如果从上次更新后这个对象有过改动,这个方法会返回true
。
if (textArea.isModified()) { textArea.commit(); }
支持此接口的组件:
-
- Collapsable-可折叠接口
-
-
addExpandedStateChangeListener()
- 添加实现了ExpandedStateChangeListener
接口的监听器来拦截组件的展开状态变化事件。@Subscribe("groupBox") protected void onGroupBoxExpandedStateChange(Collapsable.ExpandedStateChangeEvent event) { notifications.create() .withCaption("Expanded: " + groupBox.isExpanded()) .show(); }
支持此接口的组件:
-
- ComponentContainer-组件容器接口
-
-
add(component)
- 添加子组件到容器。
-
remove(component)
- 从容器移除子组件。
-
removeAll()
- 移除容器内所有组件。
-
getOwnComponent(id)
- 返回直接拥有(directly owned)的组件。
-
getComponent(id)
- 返回这个容器下面组件树的一个组件。
-
getComponentNN(id)
- 返回这个容器下面组件树的一个组件。如果没找到,会抛出异常。
-
getOwnComponents()
- 返回这个容器直接拥有的所有组件。
-
getComponents()
- 返回这个容器下的组件树的所有组件。
支持此接口的组件:
Accordion - BoxLayout - CssLayout - FieldGroup - Form - Frame - GridLayout - GroupBoxLayout - HtmlBoxLayout - ScrollBoxLayout - SplitPanel - TabSheet
-
- OrderedContainer-有序容器接口
-
-
indexOf()
- 返回给定组件在有序容器中的索引位置。
支持此接口的组件:
BoxLayout - CssLayout - Frame - GroupBoxLayout - ScrollBoxLayout -
-
- HasContextHelp-内容提示接口
-
-
setContextHelpText()
- 设置内容提示文本。如果设置的话,会为字段添加一个特殊的图标,参阅: contextHelpText。 -
setContextHelpTextHtmlEnabled()
- 定义是否用 HTML 渲染内容提示文本。参阅: contextHelpTextHtmlEnabled。 -
setContextHelpIconClickHandler()
- 设置内容提示图标点击处理函数。点击处理函数比 context help text 优先级高,也就是说,如果点击处理函数设置了的话,默认的弹出小窗便不会显示。
textArea.setContextHelpIconClickHandler(contextHelpIconClickEvent -> dialogs.createMessageDialog() .withCaption("Title") .withMessage("Message body") .withType(Dialogs.MessageType.CONFIRMATION) .show() );
几乎所有组件都支持此接口:
Accordion - BoxLayout - BrowserFrame - ButtonsPanel - Calendar - CheckBox - CheckBoxGroup - ColorPicker - CssLayout - CurrencyField - DataGrid - DateField - DatePicker - Embedded - FieldGroup - FileUploadField - Filter - Form - GridLayout - GroupBoxLayout - GroupTable - HtmlBoxLayout - Image - JavaScriptComponent - Label - LookupField - LookupPickerField - MaskedField - OptionsGroup - OptionsList - PasswordField - PickerField - PopupView - ProgressBar - RadioButtonGroup - RichTextArea - ScrollBoxLayout - SearchPickerField - SourceCodeEditor - SplitPanel - SuggestionField - SuggestionPickerField - Table - TabSheet - TextArea - TextField - TimeField - TokenList - Tree - TreeDataGrid - TreeTable - TwinColumn
-
- HasSettings-用户设置接口
-
-
applySettings()
- 恢复上次用户使用该组件的设置。 -
saveSettings()
- 保存当前用户对该组件的设置。
支持此接口的组件:
DataGrid - Filter - GroupBoxLayout - SplitPanel - Table - TextArea
-
- HasUserOriginated-事件来源接口
-
-
isUserOriginated()
- 提供事件来源的信息。如果事件是在客户端通过用户交互触发,则返回true
。如果是在服务端以编程方式触发,则返回false
。用例示范:
@Subscribe("customersTable") protected void onCustomersTableSelection(Table.SelectionEvent<Customer> event) { if (event.isUserOriginated()) notifications.create() .withCaption("You selected " + event.getSelected().size() + " customers") .show(); }
isUserOriginated()
方法支持对以下事件进行跟踪:-
CollapseEvent
,所属组件: TreeDataGrid, -
ColumnCollapsingChangeEvent
,所属组件: DataGrid, -
ColumnReorderEvent
,所属组件: DataGrid, -
ColumnResizeEvent
,所属组件: DataGrid, -
ExpandedStateChangeEvent
,所属组件: Filter 和 GroupBoxLayout (参阅 Collapsable), -
ExpandEvent
,所属组件: TreeDataGrid, -
SelectedTabChangeEvent
,所属组件: TabSheet, -
SelectionEvent
,所属组件: DataGrid, -
SelectionEvent
,所属组件: Table, -
SortEvent
,所属组件: DataGrid, -
SplitPositionChangeEvent
,所属组件: SplitPanel, -
ValueChangeEvent
,所属组件:所有实现了HasValue
接口的组件 (参阅: ValueChangeListener)。
-
- HasValue-有值处理接口
-
-
addValueChangeListener()
- 添加实现了ValueChangeListener
接口的监听器来拦截组件的值变化事件。@Inject private TextField<String> textField; @Inject private Notifications notifications; @Subscribe protected void onInit(InitEvent event) { textField.addValueChangeListener(stringValueChangeEvent -> notifications.create() .withCaption("Before: " + stringValueChangeEvent.getPrevValue() + ". After: " + stringValueChangeEvent.getValue()) .show()); }
为了达到相同的目的,也可以订阅组件特定的事件,示例:
@Subscribe("textField") protected void onTextFieldValueChange(HasValue.ValueChangeEvent<String> event) { notifications.create() .withCaption("Before: " + event.getPrevValue() + ". After: " + event.getValue()) .show(); }
也可参阅 UserOriginated.
支持此接口的组件:
CheckBox - CheckBoxGroup - ColorPicker - CurrencyField - DateField - DatePicker - FileUploadField - Label - LookupField - LookupPickerField - MaskedField - OptionsGroup - OptionsList - PasswordField - PickerField - ProgressBar - RadioButtonGroup - RichTextArea - SearchPickerField - SourceCodeEditor - SuggestionField - SuggestionPickerField - TextArea - TextField - TimeField - TokenList - TwinColumn
-
- LayoutClickNotifier-布局点击通知接口
-
-
addLayoutClickListener()
- 添加实现了LayoutClickListener
接口的监听器拦截鼠标在组件区域点击事件。vbox.addLayoutClickListener(layoutClickEvent -> notifications.create() .withCaption("Clicked") .show());
为了达到相同的目的,也可以订阅组件特定的事件,示例:
@Subscribe("vbox") protected void onVboxLayoutClick(LayoutClickNotifier.LayoutClickEvent event) { notifications.create() .withCaption("Clicked") .show(); }
支持此接口的组件:
-
- HasMargin-容器边距接口
-
-
setMargin()
- 设置容器外边框和容器内容之间的边距。-
设置组件所有方向的边距:
vbox.setMargin(true);
-
只设置上边距和下边距:
vbox.setMargin(true, false, true, false);
-
创建
MarginInfo
配置类实例来设置边距:vbox.setMargin(new MarginInfo(true, false, false, true));
-
-
getMargin()
- 以MarginInfo
实例的方式返回组件的边距设置。
支持此接口的组件:
-
- HasOuterMargin-组件外边距接口
-
-
setOuterMargin()
- 设置组件外边框外的边距。-
设置所有方向的外边距:
groupBox.setOuterMargin(true);
-
只设置组件上下外边距:
groupBox.setOuterMargin(true, false, true, false);
-
创建
MarginInfo
配置类实例来设置外边距:groupBox.setOuterMargin(new MarginInfo(true, false, false, true));
-
-
getOuterMargin()
- 以MarginInfo
实例的方式返回组件的外边距设置。
支持此接口的组件:
-
- HasSpacing-留白接口
-
-
setSpacing()
- 在这个组件和他的子组件之间添加一些空白。vbox.setSpacing(true);
支持此接口的组件:
-
- ShortcutNotifier-快捷键接口
-
-
addShortcutAction()
- 添加一个操作,当用户按下配置的快捷键组合的时候触发。cssLayout.addShortcutAction(new ShortcutAction("SHIFT-A", shortcutTriggeredEvent -> notifications.create() .withCaption("SHIFT-A action") .show()));
支持此接口的组件:
-
3.5.2.6. 组件的 XML 属性
- align - 对齐
-
定义组件跟父容器的相对位置,可选值有:
-
TOP_RIGHT
-
TOP_LEFT
-
TOP_CENTER
-
MIDDLE_RIGHT
-
MIDDLE_LEFT
-
MIDDLE_CENTER
-
BOTTOM_RIGHT
-
BOTTOM_LEFT
-
BOTTOM_CENTER
-
- box.expandRatio - 箱式占比率
-
<hbox width="500px" expand="button1" spacing="true"> <button id="button1" box.expandRatio="1"/> <button id="button2" width="100%" box.expandRatio="3"/> <button id="button3" width="100%" box.expandRatio="2"/> </hbox>
默认情况下,所有放置组件的槽段有相等的宽度或高度(即,
box.expandRatio = 1
)。如果至少有一个组件的该属性设置成别的值,则会忽略所有的隐式值,只考虑设置了的显式值。也可参阅 expand 属性。
- colspan - 占列数目
-
设置组件应占用的网格(grid columns)列数,默认是 1
这个属性可以给任何在 GridLayout 容器中的组件定义列宽。
- contextHelpText - 内容提示文字
- contextHelpTextHtmlEnabled - 是否启用 HTML 格式内容提示
-
定义内容提示是否可以通过 HTML 格式显示。
<textField id="textField" description="Description" contextHelpText="<p><h1>Lorem ipsum dolor</h1> sit amet, <b>consectetur</b> adipiscing elit.</p><p>Donec a lobortis nisl.</p>" contextHelpTextHtmlEnabled="true"/>
可选值:
true
、false
.
- css
-
为 UI 组件提供声明式的方式来设置 CSS 属性。这个属性可以跟 stylename 属性一起使用,参阅下面的例子。
XML 定义:<cssLayout css="display: grid; grid-gap: 10px; grid-template-columns: 33% 33% 33%" stylename="demo" width="100%" height="100%"> <label value="A" css="grid-column: 1 / 3; grid-row: 1"/> <label value="B" css="grid-column: 3; grid-row: 1 / 3;"/> <label value="C" css="grid-column: 1; grid-row: 2;"/> <label value="D" css="grid-column: 2; grid-row: 2;"/> </cssLayout>
附加的 CSS:.demo > .v-label { display: block; background-color: #444; color: #fff; border-radius: 5px; padding: 20px; font-size: 150%; }
- dataLoader - 数据加载器
-
设置数据加载器,为界面 XML 描述的
data
部分定义一个数据容器。
- datasource - 数据源
- datatype - 数据类型
-
如果控件不与实体属性相关联(比如没有设置数据容器和属性名称),则需要设置数据类型。该属性的值可以是注册在应用程序元数据中的一个数据类型 - 参考 数据类型接口。
这些组件可以使用该属性: TextField, DateField, DatePicker, TimeField 。
- description - 组件描述
-
当用户光标悬浮或者点击一个组件的区域的时候,在弹出框显示组件的描述信息。
- descriptionAsHtml - HTML 组件描述
-
定义是否在组件的描述中启用 HTML。如果设置为
true
,描述在浏览器中使用 HTML 做渲染,开发者需要保证没有使用有害的 HTML。如果设置为false
,内容将会按照普通文本显示。可选值 −
true
、false
。默认值false
。
- editable - 是否可编辑
-
标明这个组件的内容是否可编辑(不要跟 enable 混淆)
可选值:
true
、false
。默认值true
。是否能编辑绑定数据的组件(继承了
DatasourceComponent
或者ListComponent
)的内容也收到 security subsystem 的影响。如果安全子系统示意这个组件不能编辑,那么editable
属性的值会被忽略。
- enable - 是否启用
-
定义组件的启用/禁用状态。
如果一个组件被禁用,那么这个组件将不能操作。禁用一个容器会禁用容器内的所有组件。可选值:
true
、false
。默认所有组件都是启用的。
- expand - 延展
-
定义容器内的组件是否可以按照组件放置的方向延展占用位置至所有可用的空间。对于垂直布局的容器,这个属性会设置组件的高度(height)到 100%;对于水平布局的容器,则是 100%宽度(width)。还有,重新设置容器的大小也会影响到自动延展的组件。参考 box.expandRatio。
- height - 组件高度
-
设置组件的高度。可以用像素(pixels)或者父容器的高度的百分比。比如
100px
、100%
、50
。如果数字没有单位,默认是用像素。按照
%
来设置值表示组件会占用父容器可用空间的相应比例的高度。当设置成
AUTO
或者-1px
的时候,组件的高度会使用默认值。对于容器来说,默认的高度是由内容定义的,也就是所有嵌入组件的高度之和。
- icon - 图标
-
设置组件的图标
这个属性的值需要指定一个图标的路径,这个路径相对于主题文件夹。
icon="icons/create.png"
或者图标集里面的图标名称:
icon="CREATE_ACTION"
如果希望根据用户的语言显示不同的图标,可以在消息包里面配置图标的路径,然后在
icon
属性指定这个消息的键名,比如:icon="msg://addIcon"
在用了 Halo 主题 (或者继承了这个主题)的 web 客户端,可以用 Font Awesome 来替代图标文件。在
icon
属性中定义所需要的图标名称,名称可以从com.vaadin.server.FontAwesome
类的常量中找到,然后用font-icon:
前缀来设置,比如:icon="font-icon:BOOK"
参阅 图标 章节了解使用图标的更多细节。
- id - 组件标识符
-
设置组件的标识符。
推荐使用 Java 标识符的规则和驼峰命名法来给组件创建标识符,比如:
userGrid
、filterPanel
。任何组件都可以使用id
属性,但是需要保证在一个界面中每个组件的标识符是唯一的。
- inputPrompt - 输入提示
-
定义当组件的值是
null
的时候在这个组件显示的文字。<suggestionField inputPrompt="Let's search something!"/>
这个属性只能在 TextField 、LookupField 、 LookupPickerField 、 SearchPickerField 、 SuggestionPickerField 这些组件中使用,并且只支持 web 客户端。
- margin - 边距
-
定义组件的外边框和容器内容之间的留白。
可以有两种形式的值设置:
-
margin="true"
− 给所有方向都加了边距。 -
margin="true,false,true,false"
− 只给上下两边加了边距(值格式是:“上,右,下,左”)。
默认情况下没有边距设置。
-
- nullName - null 名称
-
选择
nullName
属性定义的名称相当于设置组件的值为null
。换言之,nullName
就是组件null
选项的显示名称。这个属性可以在 LookupField 、 LookupPickerField 和 SearchPickerField 组件使用。
在 XML 描述内设置这个属性的值:
<lookupField datasource="orderDs" property="customer" nullName="(none)" optionsDatasource="customersDs" width="200px"/>
在控制器中设置这个属性的值:
<lookupField id="customerLookupField" optionsDatasource="customersDs" width="200px" datasource="orderDs" property="customer"/>
customerLookupField.setNullOption("<null>");
- openType - 界面打开类型
-
定义一个界面要以什么方式打开。对应
WindowManager.OpenType
枚举类型的值:NEW_TAB
、THIS_TAB
、NEW_WINDOW
、DIALOG ` 。默认是 `THIS_TAB
.
- optionsContainer - 选项容器
-
设置一个数据容器的名称,这个容器包含一个选项列表。
captionProperty 属性可以跟
optionsContainer
一起使用。
- optionsDatasource - 选项数据源
-
设置包含一个选项列表的数据源。
captionProperty 属性可以跟
optionsDatasource
一起使用。
- optionsEnum - 选项枚举类型
-
设置一个含有选项列表的枚举类名称,一个枚举值就是一个选项。
- property - 属性名称
-
设置实体属性的名称,这个属性会在这个组件用来编辑和显示。
property
属性总是和数据源属性一起使用。
- required - 必须有值
-
- requiredMessage - 必须有值提醒
-
总是跟 required 一起使用。这个属性设置一个消息,当组件没有值的时候会弹出这个消息提示。
这个属性也可以用消息包的键名,比如:
requiredMessage="msg://infoTextField.requiredMessage"
- responsive - 响应式
-
设置组件是否可以根据可用的空间的大小自动适应。可以通过 styles 来定制自适应的方式。
可选值:
true
、false
。默认值false
。
- rowspan - 占用行数
-
设置组件占据的网格行数(grid lines),默认是 1。
这个属性可以给直接放在在 GridLayout 容器内的任何组件使用。
- settingsEnabled - 设置开启
-
定义用户是否可以保存/恢复对组件的设置。只有组件有 id,才能保存组件的设置。
可选值:
true
、false
。默认值true
。
- spacing - 留白
-
设置容器内组件之间是否留白。
可选值:
true
、false
。默认值false
。
- stylename - 样式名称
-
定义组件的 CSS 类名称,更多细节,参考 主题。
在
halo
主题中,有一些为组件预定义的 CSS 类名称:-
huge
- 设置组件大小为默认值的 160%
-
large
- 设置组件大小为默认值的 120%
-
small
- 设置组件大小为默认值的 85%
-
tiny
- 设置组件大小为默认值的 75%
-
- tabCaptionsAsHtml - 标签名称是否 HTML 格式
-
定义标签的标题是否可以用 HTML。如果设置成
true
,标题在浏览器里会以 HTML 的方式渲染,开发者需要保证没有使用有害的 HTML。如果设置成false
,内容会按照纯文本来渲染。可选值:
true
、false
。默认值false
。
- tabIndex - tab 键索引
-
定义组件是否可以获得焦点,同时也可以设置组件在界面上所有可获得焦点组件内部的顺序值。
可以使用正负整数:
-
负值
表示组件可以获得焦点,但是不能被顺序的键盘导航访问到; -
0
表示组件可以获得焦点,也可以被顺序的键盘访问访问到;但是它的访问顺序是由它在界面中的相对位置来定; -
正值
表示组件可以获得焦点,也可以被顺序的键盘导航访问到;它被访问的相对顺序是通过这个属性的值来定义的:按照tabIndex
升序来访问。如果几个组件有相同的tabIndex
值,它们的相对访问顺序由它们在界面的相对位置来确定。
-
- tabsVisible - 标签可见
-
设置在标签页面板中是否显示标签选择部分。
可选值:
true
、false
。默认值true
。
- textSelectionEnabled - 文本选择启用
-
定义在表格单元格中是否能选择文本。
可选值:
true
、false
。默认值false
。
- visible - 组件是否可见
-
设置组件是否可见。
如果容器被设置成不可见,那么他里面的所有组件也不可见。默认所有组件都是可见。
可选值:
true
、false
。默认值true
。
- width - 组件宽度
-
定义组件的宽度
可以用像素(pixels)或者父容器的宽度的百分比。比如
100px
、100%
、50
。如果数字没有单位,默认是用像素。按照
%
来设置值表示组件会占用父容器可用空间的相应比例的宽度。当设置成
AUTO
或者-1px
的时候,组件的宽度会使用默认值。对于容器来说,默认的宽度是由内容定义的,也就是所有嵌入组件的宽度之和。
3.5.3. 数据组件
数据组件是界面的不可见元素,用来从中间层加载数据,然后绑定数据到可视化组件,也可将数据改动保存回中间层。有以下数据组件:
通常,数据组件在界面 XML 描述的 <data>
元素定义。可以跟可视化组件以相同的方式注入到控制器中:
@Inject
private CollectionLoader<Customer> customersDl;
private String customerName;
@Subscribe
protected void onBeforeShow(BeforeShowEvent event) {
customersDl.setParameter("name", customerName)
customersDl.load();
}
特定界面的数据组件注册在 ScreenData
对象中,这个对象跟控制器关联,可以通过控制器的 getScreenData()
方法获取。这个对象在需要加载界面所有的数据的时候很有用,示例:
@Subscribe
protected void onBeforeShow(BeforeShowEvent event) {
getScreenData().loadAll();
}
需要注意的是,如果控制器带有 |
3.5.3.1. 数据容器
数据容器在数据模型和可视化组件之间形成一个薄薄的处理层。容器用来容纳实体实例和实体集合、提供实体元类型、视图和选中的集合中实体的信息,以及为各种事件注册监听器。
3.5.3.1.1. 单一实例容器
InstanceContainer
接口是数据容器层次结构的根节点。用来容纳单一实体实例,有下列方法:
-
setItem()
- 为容器设置一个实体实例。 -
getItem()
- 返回容器中保存的实例。如果容器是空的,此方法会抛出异常。所以需要在确保容器有设置实体的时候才使用此方法,然后就不需要检查返回值是否为 null。 -
getItemOrNull()
- 返回容器中保存的实例。如果容器是空的,此方法会返回 null。所以在使用此方法返回值之前总是需要先检查返回的是否是 null。 -
getEntityMetaClass()
- 返回能存储在此容器的实体的元类。 -
setView()
- 设置在加载容器实体时需要使用的视图。需要注意的是,容器本身不会加载数据,所以这个属性只是为此容器关联的数据加载器设定视图。 -
getView()
- 返回在加载容器实体时需要使用的视图。
- InstanceContainer 事件
-
使用
InstanceContainer
接口可以注册以下事件的监听器。-
ItemPropertyChangeEvent
会在容器中存储的实例的属性值发生变化时发送。下面例子展示了订阅容器的事件,该容器在界面 XML 中使用customerDc
id定义:@Subscribe(id = "customerDc", target = Target.DATA_CONTAINER) private void onCustomerDcItemPropertyChange( InstanceContainer.ItemPropertyChangeEvent<Customer> event) { Customer customer = event.getItem(); String changedProperty = event.getProperty(); Object currentValue = event.getValue(); Object previousValue = event.getPrevValue(); // ... }
-
ItemChangeEvent
会在另一个实例(或者null)设置到容器时发送。下面例子展示了订阅容器的事件,该容器在界面 XML 中使用customerDc
id定义:@Subscribe(id = "customerDc", target = Target.DATA_CONTAINER) private void onCustomerDcItemChange(InstanceContainer.ItemChangeEvent<Customer> event) { Customer customer = event.getItem(); Customer previouslySelectedCustomer = event.getPrevItem(); // ... }
-
3.5.3.1.2. 集合容器
CollectionContainer
接口用来容纳相同类型实例的集合。这个接口是 InstanceContainer
的后代,定义了以下特有的方法:
-
setItems()
- 为容器设置实体集合。 -
getItems()
- 返回容器中保存的实体的不可变列表。可以用这个方法来遍历集合、获得实体集合流或者用根据索引获取单一实例。如果需要按照实体的 id 获取实例,使用getItem(entityId)
方法。示例:@Inject private CollectionContainer<Customer> customersDc; private Optional<Customer> findByName(String name) { return customersDc.getItems().stream() .filter(customer -> Objects.equals(customer.getName(), name)) .findFirst(); }
-
getMutableItems()
- 返回容器中保存的实体的可变列表。所有对列表的改动,包括add()
、addAll()
、remove()
、removeAll()
、set()
、clear()
方法都会产生CollectionChangeEvent
事件,所以订阅了这个事件的可视化组件也会根据变化更新,示例:@Inject private CollectionContainer<Customer> customersDc; private void createCustomer() { Customer customer = metadata.create(Customer.class); customer.setName("Homer Simpson"); customersDc.getMutableItems().add(customer); }
只有在需要更改集合的时候使用
getMutableItems()
,否则应该使用getItems()
,防止意外改动。 -
setItem()
- 为容器设置 当前 实例。如果提供的内容不是 null,则必须是集合中的一个对象。此方法会发送ItemChangeEvent
。需要注意的是,类似 Table 的可视化组件不会监听容器发送的
ItemChangeEvent
事件。所以如果需要在表中选中一行,需要使用集合容器的setSelected()
方法,而不是setItem()
。容器的当前 item 也会更改,因为容器同时也监听了组件。示例:@Inject private CollectionContainer<Customer> customersDc; @Inject private GroupTable<Customer> customersTable; private void selectFirstRow() { customersTable.setSelected(customersDc.getItems().get(0)); }
-
getItem()
- 重写了InstanceContainer
的同名方法,返回 当前 实例。如果当前实例没有设置,此方法会抛出一个异常。所以需要在确保容器有选中当前实例的时候才使用此方法,然后就不需要检查返回值是否为 null。 -
getItemOrNull()
- 重写了InstanceContainer
的同名方法,返回 当前 实例。如果当前实例没有设置,此方法会返回 null。所以在使用此方法返回值之前总是需要先检查返回的是否是 null。 -
getItemIndex(entityId)
- 返回实例在getItems()
和getMutableItems()
方法返回的列表中的位置。此方法接收Object
对象,因此可以传给它 id 或者实体实例本身。容器的实现维护了一个 id 到索引的映射,所以这个方法即使在非常大的列表中也有很高效率。 -
getItem(entityId)
- 按照实例的 id 返回此实例。这个是一个快捷方法,首先用getItemIndex(entityId)
得到实例的位置,然后通过getItems().get(index)
返回实例。所以如果需要找的实例不在集合中存在,则会抛出异常。 -
getItemOrNull(entityId)
- 跟getItem(entityId)
类似,只不过在实例不存在的时候会返回 null。所以需要在使用前检查此方法的返回值是否是 null。 -
containsItem(entityId)
- 如果指定 id 的实体在集合中存在的话,返回 true。底层其实调用了getItemIndex(entityId)
方法。 -
replaceItem(entity)
- 如果在容器中有相同 id 的实例,则会被方法的输入参数的实例替换。如果不存在,则会添加新的实例到实例列表中。此方法会发送CollectionChangeEvent
事件,根据具体做了什么,事件类型可以是SET_ITEM
或者ADD_ITEMS
。 -
setSorter()
- 设置此容器的排序器。Sorter
接口的标准实现是CollectionContainerSorter
。当容器关联到加载器时,会设置默认的排序器。也可提供自定义的实现。 -
getSorter()
- 返回此容器当前设置的排序器。
- CollectionContainer 事件
-
除了 InstanceContainer 的事件之外,还可以使用
CollectionContainer
接口注册CollectionChangeEvent
事件的监听器,该事件在容器内的实体集合改动时发送,比如,添加、删除和替换集合内元素。下面例子展示了订阅容器的事件,该容器在界面 XML 中使用customersDc
id定义:@Subscribe(id = "customersDc", target = Target.DATA_CONTAINER) private void onCustomersDcCollectionChange( CollectionContainer.CollectionChangeEvent<Customer> event) { CollectionChangeType changeType = event.getChangeType(); (1) Collection<? extends Customer> changes = event.getChanges(); (2) // ... }
1 - 改动类型:REFRESH,ADD_ITEMS,REMOVE_ITEMS,SET_ITEM。 2 - 从容器中添加或者删除的实体集合。如果改动类型是 REFRESH,框架不能确定具体是哪些实体添加或者删除,所以此时该集合为空。
3.5.3.1.3. 属性容器
InstancePropertyContainer
和 CollectionPropertyContainer
是设计用来处理实体实例和集合,这些实体实例和集合是其它实体的属性。比如,如果 Order
实体有 orderLines
属性,这个属性是 OrderLine
实体的集合,那么可以使用 CollectionPropertyContainer
来绑定 orderLines
到一个表格组件。
属性容器实现了 Nested
接口,这个接口定义了获取主容器方法,以及获取此属性容器绑定的主容器的属性名称的方法。在 Order
和 OrderLine
实体的例子中,主容器是用来存储 Order
实例的容器。
InstancePropertyContainer
可以直接跟主实体的属性交互。也就是说,如果调用 setItem()
方法,这个值会直接设置到相应主实体的属性,同时主实体的 ItemPropertyChangeEvent
监听器会被触发。
CollectionPropertyContainer
包含主集合的拷贝,并且它的方法行为如下:
-
getMutableItems()
返回实体的可变列表,对列表的改动都会反映到底层的属性。也就是说,如果从列表中删除了一项,主实体的属性也会更改,主容器的ItemPropertyChangeEvent
监听器会触发。 -
getDisconnectedItems()
返回实体的可变列表,但是这个列表内的改动不会反映到底层属性。也就是说如果从这个列表中删除了一项,主实体属性不变。 -
setItems()
为容器设置实体集合,同时也设置给了关联的主属性。因此,主容器的ItemPropertyChangeEvent
监听器会被触发。 -
setDisconnectedItems()
为容器设置实体集合,但是底层关联的主属性不变。
getDisconnectedItems()
和 setDisconnectedItems()
方法可以用来暂时改变集合在 UI 的展示,比如对表格做过滤:
@Inject
private CollectionPropertyContainer<OrderLine> orderLinesDc;
private void filterByProduct(String product) {
orderLinesDc.getDisconnectedItems().removeIf(
orderLine -> !orderLine.getProduct().equals(product));
}
private void resetFilter() {
orderLinesDc.setDisconnectedItems(getEditedEntity().getOrderLines());
}
3.5.3.1.4. 键值对容器
KeyValueContainer
和 KeyValueCollectionContainer
是用来处理 KeyValueEntity
的。这个实体可以包含在运行时定义的任意数量的属性。
键值对容器定义了下列特殊的方法:
-
addProperty()
由于容器可以保存带有任意数量属性的实体,需要在使用此方法的时候指定是添加什么属性。这个方法接收属性名称和对应的类型,类型可以是数据类型格式,也可以是 Java 类。在使用 Java 类的情况下,这个类要么是实体类,要么是有数据类型支持的类。 -
setIdName()
是一个可选择调用的方法,通过这个方法可以将一个属性定义为实体的标识符属性。也就是说,保存在此容器内的KeyValueEntity
实体将使用指定的属性作为标识符。否则,KeyValueEntity
将使用随机生成的 UUID 作为标识符。 -
getEntityMetaClass()
返回MetaClass
接口的动态实现类,这个类反映了KeyValueEntity
实例的当前结构,实例的结构是通过之前调用addProperty()
来定义的。
3.5.3.2. 数据加载器
数据加载器用来从中间层加载数据到数据容器。
根据交互的数据容器不同,数据加载器的接口有稍微的不同:
-
InstanceLoader
使用实体 id 或者 JPQL 查询语句加载单一实体到InstanceContainer
。 -
CollectionLoader
使用 JPQL 查询语句加载实体集合到CollectionContainer
。可以设置分页、排序以及其它可选的参数。 -
KeyValueCollectionLoader
加载KeyValueEntity
实体的集合到KeyValueCollectionContainer
。除了CollectionLoader
参数,还可以指定一个数据存储参数。
在界面的 XML 描述中,所有的加载器都用同一个 <loader>
元素中定义,加载器的类型通过包裹它的容器类型确定。
数据加载器不是必选的,因为可以使用 DataManager
或者自定义的服务来加载数据,之后直接设置给容器。但是使用加载器通过在界面中声明式的定义可以简化数据加载的过程,特别是要使用过滤器组件的情况下。通常,集合加载器从界面的描述文件中获得 JPQL 查询语句,然后从过滤器组件拿到查询参数,之后创建 LoadContext
并且调用 DataManager
加载实体。所以,典型的 XML 描述看起来是这样:
<data>
<collection id="customersDc" class="com.company.sample.entity.Customer" view="_local">
<loader id="customersDl">
<query>
select e from sample_Customer e
</query>
</loader>
</collection>
</data>
<layout>
<filter id="filter" applyTo="customersTable" dataLoader="customersDl">
<properties include=".*"/>
</filter>
<!-- ... -->
</layout>
loader
XML 元素的属性可以用来定义可选参数,比如 cacheable
、 softDeletion
等。
在实体编辑界面,加载器的 XML 元素通常是空的,因为实例加载器需要一个实体的标识符,这个标识符通过编程的方式使用 StandardEditor
基类指定。
<data>
<instance id="customerDc" class="com.company.sample.entity.Customer" view="_local">
<loader/>
</instance>
</data>
加载器可以将实际的加载动作代理到一个函数,这个函数可以通过 setLoadDelegate()
方法或者通过在界面控制器中使用 @Install
注解来声明式的提供。示例:
@Inject
private DataManager dataManager;
@Install(to = "customersDl", target = Target.DATA_LOADER)
protected List<Customer> customersDlLoadDelegate(LoadContext<Customer> loadContext) {
return dataManager.loadList(loadContext);
}
在上面的例子中,customersDl
加载器会使用 customersDlLoadDelegate()
方法来加载 Customer
实体列表。此方法接收 LoadContext
参数,加载器会按照它的参数(查询语句、过滤器等等)来创建这个参数。在这个例子中,数据加载是通过 DataManager
来完成的,这个实现跟标准加载器的实现一样高效,但是好处是可以使用自定义的服务或者可以在加载完实体之后做其它的事情。
一个加载器也可以通过编程的方式创建和配置,示例:
@Inject
private DataComponents dataComponents;
private void createCustomerLoader(CollectionContainer<Customer> container) {
CollectionLoader<Customer> loader = dataComponents.createCollectionLoader();
loader.setQuery("select e from sample_Customer e");
loader.setContainer(container);
loader.setDataContext(getScreenData().getDataContext());
}
当加载器设置了 DataContext (当使用 XML 描述定义加载器的时候是默认设置的),所有加载的实体都自动合并到数据上下文(data context)。
- 查询条件
-
有时需要在运行时修改数据加载器的查询语句,以便过滤数据库级别加载的数据。需要根据用户输入的参数进行过滤,最简单的方法就是将过滤器可视化组件与数据加载器关联起来。
不需要使用全局过滤器或者添加全局过滤器,而是可以为加载器查询语句单独创建一组过滤条件。一个过滤条件是一组带有参数的查询语句片段。在片段中所有的参数都设置了之后 ,这些片段才会被添加到生成的查询语句文本中。过滤条件会在数据存储级别传递,因此可以包含各个数据存储支持的不同语言的片段。框架会提供 JPQL 的过滤条件。
作为例子,考虑按照
Customer
实体的两个属性:stringname
和 booleanstatus
对实体进行过滤,看看如何创建一组过滤条件。加载器的查询过滤条件可以通过
<condition>
XML 元素进行声明式的定义,或者通过setCondition()
方法编程式的定义。下面是在 XML 中配置条件的示例:<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd" xmlns:c="http://schemas.haulmont.com/cuba/screen/jpql_condition.xsd" (1) caption="Customers browser" focusComponent="customersTable"> <data> <collection id="customersDc" class="com.company.demo.entity.Customer" view="_local"> <loader id="customersDl"> <query><![CDATA[select e from demo_Customer e]]> <condition> (2) <and> (3) <c:jpql> (4) <c:where>e.name like :name</c:where> </c:jpql> <c:jpql> <c:where>e.status = :status</c:where> </c:jpql> </and> </condition> </query> </loader> </collection> </data>
1 - 添加 JPQL 条件命名空间 2 - 在 query
内定义condition
元素3 - 如果有多个条件,添加 and
或or
元素4 - 使用可选的 join
元素和必须的where
元素定义 JPQL 条件假设界面有两个 UI 组件用来输入条件参数:
nameFilterField
文本控件和statusFilterField
复选框。为了在用户改变它们值的时候刷新数据,需要在界面控制器添加事件监听器:@Inject private CollectionLoader<Customer> customersDl; @Subscribe("nameFilterField") private void onNameFilterFieldValueChange(HasValue.ValueChangeEvent<String> event) { if (event.getValue() != null) { customersDl.setParameter("name", "(?i)%" + event.getValue() + "%"); (1) } else { customersDl.removeParameter("name"); } customersDl.load(); } @Subscribe("statusFilterField") private void onStatusFilterFieldValueChange(HasValue.ValueChangeEvent<Boolean> event) { if (event.getValue()) { customersDl.setParameter("status", true); } else { customersDl.removeParameter("status"); } customersDl.load(); }
1 - 注意这里怎么使用 ORM 提供的不区分大小写的子串搜索 如上面所说,只有在条件的参数都设置了之后才会将条件添加到查询语句中。所以在数据库会执行什么样的查询语句依赖于在 UI 组件如何输入参数:
只有 nameFilterField 有值select e from demo_Customer e where e.name like :name
只有 statusFilterField 有值select e from demo_Customer e where e.status = :status
nameFilterField 和 statusFilterField 都有值select e from demo_Customer e where (e.name like :name) and (e.status = :status)
3.5.3.3. 数据上下文
DataContext
是跟踪加载到客户端层实体改动的接口。跟踪实体的任何属性修改后都标记成 “dirty”,然后 DataContext
会在调用 commit()
方法的时候将 “脏” 实体发送到中间件进行保存。
在 DataContext
内,具有唯一标识符的实体总是以单一的对象实例呈现,不管对象关系图中它在哪里被使用或者使用了多少次。
为了能跟踪实体变化,必须使用其 merge()
方法将实体放入 DataContext
中。如果数据上下文不包含同样id的实体,则会创建一个新实例,将传递的实体状态拷贝至新实例,并将新实例返回。如果上下文已经有同样id的实例,则会将传递实例的状态拷贝至已经存在的实例并返回。使用这个机制保证在数据上下文中对于同一个实例id始终只有一个实例。
当合并实体时,实体内包含根节点的整个实体对象关系图都会被合并。也就是说,所有的引用实体(包括集合)都会处于被跟踪状态。
使用 |
合并实体到 DataContext
的示例:
@Inject
private DataContext dataContext;
private void loadCustomer(Id<Customer, UUID> customerId) {
Customer customer = dataManager.load(customerId).one();
Customer trackedCustomer = dataContext.merge(customer);
customersDc.getMutableItems().add(trackedCustomer);
}
对于一个特定的界面和它所有的内嵌的组件来说,只存在一个 DataContext
单例,在界面 XML 描述存在 <data>
元素的情况下创建。
<data>
元素可以有 readOnly="true"
属性,此时会使用一个特殊的 “不操作“ 的实现,此实现不需要跟踪实体的改动,因此不会影响性能。默认情况下,Studio 生成的实体浏览界面会有只读的数据上下文,所以如果需要在实体浏览界面跟踪实体改动并且提交脏实体,需要再删除 XML 的 readOnly="true"
属性。
- 父数据上下文
-
DataContext
实例支持父子关系。如果一个DataContext
有父上下文,它会将改动的实体提交给父上下文而不是提交给中间件。通过这个功能支持编辑组合关系,从实体只能跟主实体一起保存到数据库。如果一个实体属性使用 @Composition 注解,平台会自动在此属性的编辑界面设置父上下文,从而该属性的改动会保存到主实体的数据上下文。可以很容易为任何实体和界面提供与此相同的行为。
如果打开的编辑界面需要提交数据到当前界面的数据上下文,可以使用 builder 的
withParentDataContext()
方法:@Inject private EditorScreens editorScreens; @Inject private DataContext dataContext; private void editFooWithCurrentDataContextAsParent() { FooEdit fooEdit = editorScreens.builder(Foo.class, this) .withScreen(FooEdit.class) .withParentDataContext(dataContext) .build(); fooEdit.show(); }
如果使用
Screens
bean 打开简单界面,需要提供 setter 方法接收父数据上下文:public class FooScreen extends Screen { @Inject private DataContext dataContext; public void setParentDataContext(DataContext parentDataContext) { dataContext.setParent(parentDataContext); } }
然后在创建了界面之后使用:
@Inject private Screens screens; @Inject private DataContext dataContext; private void openFooScreenWithCurrentDataContextAsParent() { FooScreen fooScreen = screens.create(FooScreen.class); fooScreen.setParentDataContext(dataContext); fooScreen.show(); }
确保父数据上下文没有使用
readOnly="true"
属性。否则在使用这个上下文作为父上下文的时候会抛出异常。
3.5.3.4. 使用数据组件
在本章节将展示使用数据组件的实战例子。
3.5.3.4.1. 声明式创建数据组件
为界面创建数据组件的最简单方法就是在界面的 XML 描述中的 <data>
元素中进行声明式的定义。
考虑包含 Customer
、 Order
和 OrderLine
实体的数据模型。Order
实体的编辑界面可以用下面的 XML 定义:
<data>
<instance id="orderDc" class="com.company.sales.entity.Order" view="order-edit">
<loader/>
<collection id="linesDc" property="lines"/>
</instance>
<collection id="customersDc" class="com.company.sales.entity.Customer" view="_minimal">
<loader>
<query><![CDATA[select e from sales_Customer e]]></query>
</loader>
</collection>
</data>
这个例子中,会创建下列数据组件:
-
DataContext
实例。 -
使用
orderDc
作为 id 的InstanceContainer
以及InstanceLoader
,用来加载Order
实体。 -
为
OrderLines
实体创建的使用linesDc
作为 id 的CollectionPropertyContainer
。绑定到Order.lines
集合属性。 -
为
Customer
实体创建的使用customersDc
id 的CollectionContainer
。通过CollectionLoader
使用指定的查询语句加载。
数据容器可以在可视化组件中这样使用:
<layout>
<dateField dataContainer="orderDc" property="date"/> (1)
<form id="form" dataContainer="orderDc"> (2)
<column>
<textField property="amount"/>
<lookupPickerField id="customerField" property="customer"
optionsContainer="customersDc"/> (3)
</column>
</form>
<table dataContainer="linesDc"> (4)
<columns>
<column id="product"/>
<column id="quantity"/>
</columns>
</table>
1 | 单独的控件具有 dataContainer 和 property 属性。 |
2 | form 会将 dataContainer 传递给 form 的字段,所以字段只需要 property 属性。 |
3 | 查找字段有 optionsContainer 属性。 |
4 | 表格只有 dataContainer 属性。 |
3.5.3.4.2. 编程式创建数据组件
可以使用编程的方式在可视化组件中创建和使用数据组件。
下面的例子中,创建了跟前一章一样的编辑界面,使用了相同的数据和可视化组件,只不过是用纯 Java 实现的。
package com.company.sales.web.order;
import com.company.sales.entity.Customer;
import com.company.sales.entity.Order;
import com.company.sales.entity.OrderLine;
import com.haulmont.cuba.core.global.View;
import com.haulmont.cuba.gui.UiComponents;
import com.haulmont.cuba.gui.components.*;
import com.haulmont.cuba.gui.components.data.options.ContainerOptions;
import com.haulmont.cuba.gui.components.data.table.ContainerTableItems;
import com.haulmont.cuba.gui.components.data.value.ContainerValueSource;
import com.haulmont.cuba.gui.model.*;
import com.haulmont.cuba.gui.screen.PrimaryEditorScreen;
import com.haulmont.cuba.gui.screen.StandardEditor;
import com.haulmont.cuba.gui.screen.Subscribe;
import com.haulmont.cuba.gui.screen.UiController;
import javax.inject.Inject;
import java.sql.Date;
@UiController("sales_Order.edit")
public class OrderEdit extends StandardEditor<Order> {
@Inject
private DataComponents dataComponents; (1)
@Inject
private UiComponents uiComponents;
private InstanceContainer<Order> orderDc;
private CollectionPropertyContainer<OrderLine> linesDc;
private CollectionContainer<Customer> customersDc;
private InstanceLoader<Order> orderDl;
private CollectionLoader<Customer> customersDl;
@Subscribe
protected void onInit(InitEvent event) {
createDataComponents();
createUiComponents();
}
private void createDataComponents() {
DataContext dataContext = dataComponents.createDataContext();
getScreenData().setDataContext(dataContext); (2)
orderDc = dataComponents.createInstanceContainer(Order.class);
orderDl = dataComponents.createInstanceLoader();
orderDl.setContainer(orderDc); (3)
orderDl.setDataContext(dataContext); (4)
orderDl.setView("order-edit");
linesDc = dataComponents.createCollectionContainer(
OrderLine.class, orderDc, "lines"); (5)
customersDc = dataComponents.createCollectionContainer(Customer.class);
customersDl = dataComponents.createCollectionLoader();
customersDl.setContainer(customersDc);
customersDl.setDataContext(dataContext);
customersDl.setQuery("select e from sales_Customer e"); (6)
customersDl.setView(View.MINIMAL);
}
private void createUiComponents() {
DateField<Date> dateField = uiComponents.create(DateField.TYPE_DATE);
getWindow().add(dateField);
dateField.setValueSource(new ContainerValueSource<>(orderDc, "date")); (7)
Form form = uiComponents.create(Form.class);
getWindow().add(form);
LookupPickerField<Customer> customerField = uiComponents.create(LookupField.of(Customer.class));
form.add(customerField);
customerField.setValueSource(new ContainerValueSource<>(orderDc, "customer"));
customerField.setOptions(new ContainerOptions<>(customersDc)); (8)
TextField<Integer> amountField = uiComponents.create(TextField.TYPE_INTEGER);
amountField.setValueSource(new ContainerValueSource<>(orderDc, "amount"));
Table<OrderLine> table = uiComponents.create(Table.of(OrderLine.class));
getWindow().add(table);
getWindow().expand(table);
table.setItems(new ContainerTableItems<>(linesDc)); (9)
Button okButton = uiComponents.create(Button.class);
okButton.setAction(getWindow().getActionNN(WINDOW_COMMIT_AND_CLOSE));
getWindow().add(okButton);
Button cancelButton = uiComponents.create(Button.class);
cancelButton.setAction(getWindow().getActionNN(WINDOW_CLOSE));
getWindow().add(cancelButton);
}
@Override
protected InstanceContainer<Order> getEditedEntityContainer() { (10)
return orderDc;
}
@Subscribe
protected void onBeforeShow(BeforeShowEvent event) { (11)
orderDl.load();
customersDl.load();
}
}
1 | DataComponents 是创建数据组件的工厂。 |
2 | DataContext 实例在界面注册,以便标准的提交动作能正常工作。 |
3 | orderDl 加载器会加载数据到 orderDc 容器。 |
4 | orderDl 加载器会合并加载的实体到数据上下文以便跟踪改动。 |
5 | linesDc 创建为属性容器。 |
6 | 给 customersDl 加载器指定了一个查询语句。 |
7 | ContainerValueSource 用来绑定单一字段到容器。 |
8 | ContainerOptions 用来为查找控件提供选项。 |
9 | ContainerTableItems 用来绑定表格到容器。 |
10 | getEditedEntityContainer() 被重写了,用来指定容器,替代了 @EditedEntityContainer 注解。 |
11 | 在界面展示前加载数据。平台会自动设置编辑实体的 id 到 orderDl 。 |
3.5.3.4.3. 数据组件之间的依赖
有时候需要加载和展示依赖同一界面上其它数据的数据。比如,在下面的截屏中,左边的表格展示 orders 的列表,右边的表格展示选中 order 的 lines。右边的列表会在左边列表每次选择改动时刷新。
这个例子中,Order
实体包含了 orderLines
属性,这个是一对多的集合。所以实现这个界面的最简单的方法就是使用带有 orderLines
属性的视图加载 orders 列表,并且使用属性容器来装载依赖的 lines 列表。然后绑定左边的表格到主容器,绑定右边的表格到属性容器。
但是这个方案有一个隐藏的性能问题:会加载左边表格所有 orders 的所有 lines,尽管每次只是给单一的 order 展示 lines。orders 列表越长,会加载越多不需要的数据,因为用户只有很小的可能会查看每个 order 关联的 lines。这就是为什么推荐只在加载单一主实体的时候使用属性容器以及范围广的视图,比如在 order 编辑界面。
还有,主实体也许跟依赖的实体没有直接的属性关联关系。这种情况下,上面使用属性容器的方案根本就行不通。
组织界面内数据关系的通常方法是使用带参数的查询。依赖的加载器包含一个带参数的查询语句,这个参数关联到主实体的数据,当主容器的当前实体更改时,需要手动设置参数并且触发依赖的加载器。
下面这个例子的界面包含两对依赖的容器/加载器以及绑定的表格。
<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd">
<data>
<collection id="ordersDc" (1)
class="com.company.sales.entity.Order" view="order-with-customer">
<loader id="ordersDl">
<query>select e from sales_Order e></query>
</loader>
</collection>
<collection id="orderLinesDc" (2)
class="com.company.sales.entity.OrderLine" view="_local">
<loader id="orderLinesDl">
<query>select e from sales_OrderLine e where e.order = :order</query>
</loader>
</collection>
</data>
<layout>
<hbox id="mainBox" width="100%" height="100%" spacing="true">
<table id="ordersTable" width="100%" height="100%"
dataContainer="ordersDc"> (3)
<columns>
<column id="customer"/>
<column id="date"/>
<column id="amount"/>
</columns>
<rows/>
</table>
<table id="orderLinesTable" width="100%" height="100%"
dataContainer="orderLinesDc"> (4)
<columns>
<column id="product"/>
<column id="quantity"/>
</columns>
<rows/>
</table>
</hbox>
</layout>
</window>
1 | 主容器和主加载器 |
2 | 依赖容器和加载器 |
3 | 主表格 |
4 | 从表格 |
package com.company.sales.web.order;
import com.company.sales.entity.Order;
import com.company.sales.entity.OrderLine;
import com.haulmont.cuba.gui.model.CollectionLoader;
import com.haulmont.cuba.gui.model.InstanceContainer;
import com.haulmont.cuba.gui.screen.*;
import javax.inject.Inject;
@UiController("order-list")
@UiDescriptor("order-list.xml")
@LookupComponent("ordersTable")
public class OrderList extends StandardLookup<Order> { (1)
@Inject
private CollectionLoader<Order> ordersDl;
@Inject
private CollectionLoader<OrderLine> orderLinesDl;
@Subscribe
protected void onBeforeShow(BeforeShowEvent event) {
ordersDl.load(); (2)
}
@Subscribe(id = "ordersDc", target = Target.DATA_CONTAINER)
protected void onOrdersDcItemChange(InstanceContainer.ItemChangeEvent<Order> event) {
orderLinesDl.setParameter("order", event.getItem()); (3)
orderLinesDl.load();
}
}
1 | 界面控制器类没有 @LoadDataBeforeShow 注解,所以加载器不会自动触发。 |
2 | 主加载器在 BeforeShowEvent 处理器中触发。 |
3 | 在主容器的 ItemChangeEvent 处理器中,给依赖加载器设置了参数并且触发依赖加载。 |
3.5.3.4.4. 在数据加载器中使用界面参数
很多时候需要根据界面传递的参数加载界面需要的数据。下面是一个浏览界面的示例,使用了界面参数并且在加载数据时使用参数来过滤数据。
假设有两个实体:Country
和 City
。City
实体有 country
属性,是 Country
的引用。在 city 的浏览界面,可以接受一个 country 的实例,然后只展示该 country 的 city。
首先,看看 city 界面的 XML 描述。其数据加载器包含一个带有参数的查询:
<collection id="citiesDc"
class="com.company.demo.entity.City"
view="_local">
<loader id="citiesDl">
<query>
<![CDATA[select e from demo_City e where e.country = :country]]>
</query>
</loader>
</collection>
city 界面控制器有一个 public 的参数setter,然后在 BeforeShowEvent 处理器中使用了参数。注意,该界面没有 @LoadDataBeforeShow
注解,因为需要显式的触发数据加载:
@UiController("demo_City.browse")
@UiDescriptor("city-browse.xml")
@LookupComponent("citiesTable")
public class CityBrowse extends StandardLookup<City> {
@Inject
private CollectionLoader<City> citiesDl;
private Country country;
public void setCountry(Country country) {
this.country = country;
}
@Subscribe
private void onBeforeShow(BeforeShowEvent event) {
if (country == null)
throw new IllegalStateException("country parameter is null");
citiesDl.setParameter("country", country);
citiesDl.load();
}
}
city 界面可以从其他界面为其传递一个 country 实例并打开,示例:
@Inject
private ScreenBuilders screenBuilders;
private void showCitiesOfCountry(Country country) {
CityBrowse cityBrowse = screenBuilders.screen(this)
.withScreenClass(CityBrowse.class)
.build();
cityBrowse.setCountry(country);
cityBrowse.show();
}
3.5.4. 操作以及操作接口
Action
是一个接口,这个接口是对可视化组件的操作(换句话说,一些功能)的抽象。当从不同的可视化组件中调用相同的操作时(例如,分别从按钮和表格右键菜单中调用同一个操作),它特别有用。此外,此接口允许为操作提供其它属性,例如名称、可访问性和可见性标志等。
下面是 Action
接口方法:
-
actionPerform()
- 方法由与此操作关联的可视化组件调用。调用者实例被传递给该方法。 -
getId()
- 方法返回操作的标识符。标识符通常在实现Action
的类的构造函数中设置,并且在操作对象创建后的整个生命周期中不会更改。 -
获取和设置
caption
、description
、shortcut
、icon
、enabled
、visible
属性的方法。通常,相关可视化组件使用所有这些属性来设置其自身相应的属性。 -
addPropertyChangeListener()
、removePropertyChangeListener()
方法用于添加和删除处理上述属性更改的监听器。监听器接收java.beans.PropertyChangeEvent
类型的通知,其中包含已更改属性的名称 、 旧值和新值。 -
refreshState()
- 一个在特定的操作类中实现的方法,根据一些外部因素(例如用户权限)初始化上述属性。它通常在实现类的构造函数或从相关可视化组件中调用。 -
addOwner()
、removeOwner()
、getOwner()
、getOwners()
– 用于控制操作和可视化组件之间关系的方法。
建议使用声明式创建或继承BaseAction类来实现操作。此外,还有一组适用于表格和选择器组件的标准操作。
与操作关联的可视化组件可以有两种类型:
-
只单个操作的可视化组件,这类组件实现
Component.ActionOwner
接口。这类组件有Button和LinkButton。通过调用组件的
ActionOwner.setAction()
方法反操作链接到组件。此时,组件使用操作的属性设置自身相应的属性。(有关详细信息,请参阅组件概述)。 -
包含多个操作的可视化组件,这类组件实现
Component.ActionsHolder
接口。这类组件有Window
、Fragment
、DataGrid 、 Table及其继承者, Tree ,PopupButton,PickerField,LookupPickerField。ActionsHolder.addAction()
方法用于向组件添加操作。在组件中实现此方法会检查它是否已包含具有相同标识符的操作。如果包含,则现有操作将替换为新操作。因此,可以在界面描述中声明标准操作,然后在控制器中创建具有不同属性的新操作,并将其添加到组件中。
3.5.4.1. 声明式创建操作
可以在 XML 界面描述中为任何实现了 Component.ActionsHolder
接口的组件指定一组操作,包括整个窗口或 frame。 操作的定义使用 actions
元素完成,它包含嵌套的 action
元素。
action
元素有以下属性:
-
id
− 标识符,在ActionsHolder
组件中应该是唯一的。 -
caption
– 操作名称。 -
description
– 操作描述。 -
enable
– 可用性标识(true
/false
)。 -
icon
– 操作图标。
-
primary
- 属性,表明是否应使用特殊视觉样式(true
/false
)突出显示表示此操作的按钮。突出显示在
hover
主题中默认可用; 要在halo
主题中启用此功能,请将$cuba-highlight-primary-action
样式变量设置为true
。默认情况下,
create
标准列表操作和查找界面中的lookupSelectAction
是突出显示的。 -
shortcut
- 快捷键。可以在 XML 描述中对快捷键值进行硬编码。可选的修饰键:
ALT
、CTRL
、SHIFT
,由“ - ”字符分隔。例如:<action id="create" shortcut="ALT-N"/>
要避免使用硬编码值,可以使用下面列表中的预定义快捷键别名,例如:
<action id="edit" shortcut="${TABLE_EDIT_SHORTCUT}"/>
-
TABLE_EDIT_SHORTCUT
-
COMMIT_SHORTCUT
-
CLOSE_SHORTCUT
-
FILTER_APPLY_SHORTCUT
-
FILTER_SELECT_SHORTCUT
-
NEXT_TAB_SHORTCUT
-
PREVIOUS_TAB_SHORTCUT
-
PICKER_LOOKUP_SHORTCUT
-
PICKER_OPEN_SHORTCUT
-
PICKER_CLEAR_SHORTCUT
另一种选择是使用
Config
接口和方法的完全限定名称,这个方法返回快捷键定义:<action id="remove" shortcut="${com.haulmont.cuba.client.ClientConfig#getTableRemoveShortcut}"/>
-
-
visible
– 可见性标识 (true
/false
).
下面是操作声明和处理的示例。
-
为整个界面声明操作:
<window> <actions> <action id="sayHello" caption="msg://sayHello" shortcut="ALT-T"/> </actions> <layout> <button action="sayHello"/> </layout> </window>
// controller @Inject private Notifications notifications; @Subscribe("sayHello") protected void onSayHelloActionPerformed(Action.ActionPerformedEvent event) { notifications.create().setCaption("Hello").setType(Notifications.NotificationType.HUMANIZED).show(); }
在上面的示例中,声明了一个操作,它的标识符是
sayHello
,标题来自界面的消息包。此操作被绑定到一个按钮,按钮的标题将被设置为操作的名称。界面控制器订阅操作的ActionPerformedEvent
,这样当用户单击按钮或按下 ALT-T 快捷键时,将调用onSayHelloActionPerformed()
方法。 -
为PopupButton声明操作:
<popupButton id="sayBtn" caption="Say"> <actions> <action id="hello" caption="Say Hello"/> <action id="goodbye" caption="Say Goodbye"/> </actions> </popupButton>
// controller @Inject private Notifications notifications; private void showNotification(String message) { notifications.create() .withCaption(message) .withType(NotificationType.HUMANIZED) .show(); } @Subscribe("sayBtn.hello") private void onSayBtnHelloActionPerformed(Action.ActionPerformedEvent event) { notifications.create() .withCaption("Hello") .show(); } @Subscribe("sayBtn.goodbye") private void onSayBtnGoodbyeActionPerformed(Action.ActionPerformedEvent event) { notifications.create() .withCaption("Hello") .show(); }
-
为Table声明操作:
<groupTable id="customersTable" width="100%" dataContainer="customersDc"> <actions> <action id="create" type="create"/> <action id="edit" type="edit"/> <action id="remove" type="remove"/> <action id="copy" caption="Copy" icon="COPY" trackSelection="true"/> </actions> <columns> <!-- --> </columns> <rowsCount/> <buttonsPanel alwaysVisible="true"> <!-- --> <button action="customersTable.copy"/> </buttonsPanel> </groupTable>
// controller @Subscribe("customersTable.copy") protected void onCustomersTableCopyActionPerformed(Action.ActionPerformedEvent event) { // ... }
在这个例子中,除了表格的
create
、edit
和remove
标准动作之外,还声明了copy
操作。trackSelection="true"
属性表示如果表格中没有行被选中,则操作和相应按钮将被禁用。如果要对当前选定的表格行执行操作, 这个属性就很有用。 -
声明PickerField操作:
<pickerField id="userPickerField" dataContainer="customerDc" property="user"> <actions> <action id="lookup" type="picker_lookup"/> <action id="show" description="Show user" icon="USER"/> </actions> </pickerField>
// controller @Subscribe("userPickerField.show") protected void onUserPickerFieldShowActionPerformed(Action.ActionPerformedEvent event) { // }
在上面的例子中,为
PickerField
组件声明了标准的picker_lookup
操作和一个额外的show
操作。由于显示操作的PickerField
按钮使用图标而不是标题,因此未设置标题属性。description
属性允许将光标悬停在操作按钮上时显示提示信息。
在界面控制器中可以通过直接注入或从实现 Component.ActionsHolder
接口的组件中获取中任何已声明操作的引用。这对于以编程方式设置操作属性非常有用。例如:
@Named("customersTable.copy")
private Action customersTableCopy;
@Inject
private PickerField<User> userPickerField;
@Subscribe
protected void onBeforeShow(BeforeShowEvent event) {
customersTableCopy.setEnabled(false);
userPickerField.getActionNN("show").setEnabled(false);
}
3.5.4.2. 标准操作
框架提供了一些标准操作用于处理常见任务,例如为表格中选择的实体调用编辑界面。通过在 type
属性中指定其类型,就可以在界面 XML 描述中声明标准操作,例如:
<!-- in a table -->
<action type="create"/>
<!-- in a PickerField -->
<action id="lookup" type="picker_lookup"/>
标准操作根据自身的类型和所关联的组件对自身进行配置,有情况下需要指定额外的配置参数。
标准操作分为两类:
可以在项目中创建相似的操作或覆盖现有的标准操作类型。
例如,假设需要一个操作来显示表中当前所选实体的实例名称,并且希望通过仅指定其类型在多个界面中使用此操作。以下是创建此类操作的步骤。
-
创建一个操作类添加
@ActionType
注解,在这个注解里指定操作名称:package com.company.sample.web.actions; import com.haulmont.cuba.core.entity.Entity; import com.haulmont.cuba.core.global.MetadataTools; import com.haulmont.cuba.gui.ComponentsHelper; import com.haulmont.cuba.gui.Notifications; import com.haulmont.cuba.gui.components.ActionType; import com.haulmont.cuba.gui.components.Component; import com.haulmont.cuba.gui.components.actions.ItemTrackingAction; import javax.inject.Inject; @ActionType("showSelected") public class ShowSelectedAction extends ItemTrackingAction { @Inject private MetadataTools metadataTools; public ShowSelectedAction(String id) { super(id); setCaption("Show Selected"); } @Override public void actionPerform(Component component) { Entity selected = getTarget().getSingleSelected(); if (selected != null) { Notifications notifications = ComponentsHelper.getScreenContext(target).getNotifications(); notifications.create() .setType(Notifications.NotificationType.TRAY) .setCaption(metadataTools.getInstanceName(selected)) .show(); } } }
-
在
web-spring.xml
文件中,添加<gui:actions>
元素,其中base-packages
属性指向一个包,在这个包里可以找到带注解的操作类:<beans ... xmlns:gui="http://schemas.haulmont.com/cuba/spring/cuba-gui.xsd"> <!-- ... --> <gui:actions base-packages="com.company.sample.web.actions"/> </beans>
-
现在,可以通过指定操作类型的名称在界面描述中声明这个操作:
<groupTable id="customersTable"> <actions> <action id="show" type="showSelected"/> </actions> <columns> <!-- ... --> </columns> <buttonsPanel> <button action="customersTable.show"/> </buttonsPanel> </groupTable>
3.5.4.2.1. 列表组件操作
框架为实现了 ListComponent
接口的可视化组件(DataGrid 、 Table 、 GroupTable 、 TreeTable 和 Tree)提供了一组标准操作,这些操作在 com.haulmont.cuba.gui.actions.list
包中。
在表格中使用标准操作的示例:
<groupTable id="customersTable" width="100%" dataContainer="customersDc">
<actions>
<action id="create" type="create"/>
<action id="edit" type="edit"/>
<action id="remove" type="remove"/>
</actions>
<columns>
<column id="name"/>
<column id="email"/>
</columns>
<buttonsPanel>
<button id="createBtn" action="customersTable.create"/>
<button id="editBtn" action="customersTable.edit"/>
<button id="removeBtn" action="customersTable.remove"/>
</buttonsPanel>
</groupTable>
标准列表组件操作包括以下类型:
-
create
- 类型由com.haulmont.cuba.gui.actions.list.CreateAction
类实现。它被设计用于使用其默认编辑界面创建新实体。 -
edit
- 类型由com.haulmont.cuba.gui.actions.list.EditAction
类实现。它被设计用于使用其默认编辑界面编辑所选实体。 -
remove
- 类型由com.haulmont.cuba.gui.actions.list.RemoveAction
类实现。它被设计用于删除所选实体。 -
add
- 类型由com.haulmont.cuba.gui.actions.list.AddAction
类实现。它被设计用于从默认查找界面选取实体然后将其添加到关联的数据容器。这个操作典型的用例是用来为多对多集合添加实体。 -
exclude
- 类型由com.haulmont.cuba.gui.actions.list.ExcludeAction
类实现。它被设计用于从集合数据容器中删除实体但是不从数据库删除。这个操作典型的用例是从多对多集合中删除实体。 -
refresh
- 类型由com.haulmont.cuba.gui.actions.list.RefreshAction
类实现。它被设计用于重新加载列表组件使用的数据容器。 -
excel
- 类型由com.haulmont.cuba.gui.actions.list.ExcelAction
类实现。它被设计用于将列表组件内容输出到 XLS 文件。
标准操作为基本参数(题、图标和快捷键)提供默认值以及执行时的默认行为。可以在 XML 中为基本参数提供自己的值,就像其它任何操作一样。例如,可以指定自定义图标:
<action id="create" type="create" icon="USER"/>
要自定义执行行为,应该订阅操作的 ActionPerformedEvent
。如果提供了自己的操作监听器,则所有标准操作都不会执行。这意味着 自定义的 ActionPerformedEvent
处理器已经覆盖了默认的操作行为。
例如,以下代码将覆盖默认的 create
操作行为,以使用以模式对话框打开的特定界面创建 Customer
实体:
public class CustomerBrowse extends StandardLookup<Customer> {
@Inject
private GroupTable<Customer> customersTable;
@Inject
private ScreenBuilders screenBuilders;
@Subscribe("customersTable.create")
protected void onCustomersTableCreateActionPerformed(Action.ActionPerformedEvent event) {
screenBuilders.editor(customersTable)
.newEntity()
.withScreenClass(CustomerEdit.class) // specific editor screen
.withLaunchMode(OpenMode.DIALOG) // open as modal dialog
.build()
.show();
}
}
跟自定义 create
一样,可以对 edit
的行为进行定制。对有关更多详细信息,请参阅 ScreenBuilders bean 描述.
add
行为使用的是 ScreenBuilders
bean,所以可以按照下面的方式进行定制:
public class CustomerEdit extends StandardEditor<Customer> {
@Inject
private ScreenBuilders screenBuilders;
@Inject
private Table<Employee> accountableTable;
@Subscribe("accountableTable.add")
protected void onAccountableTableAddActionPerformed(Action.ActionPerformedEvent event) {
screenBuilders.lookup(Employee.class, this)
.withListComponent(accountableTable)
.withScreenClass(EmployeeBrowse.class) // specific editor screen
.withLaunchMode(OpenMode.DIALOG) // open as modal dialog
.build()
.show();
}
}
3.5.4.2.2. 选择器组件操作
框架为 PickerField 、LookupPickerField 和 SearchPickerField组件提供了一系列标准操作。
在选择器组件中使用标准操作的示例:
<pickerField id="userPickerField" dataContainer="employeeDc" property="user">
<actions>
<action id="lookup" type="picker_lookup"/>
<action id="open" type="picker_open"/>
<action id="clear" type="picker_clear"/>
</actions>
</pickerField>
标准选择器组件操作包括以下类型:
-
picker_lookup
- 类型由com.haulmont.cuba.gui.actions.picker.LookupAction
类实现。它被设计用于从查找界面中选择实体实例并将其设置到选择器字段中。 -
picker_open
- 类型由com.haulmont.cuba.gui.actions.picker.OpenAction
类实现。它被设计用于为选择器字段中当前选定的实体打开编辑界面。 -
picker_clear
- 类型由com.haulmont.cuba.gui.actions.picker.ClearAction
类实现。它被设计用于清空选择器。
标准操作为基本参数(标题、图标和快捷方键)提供默认值和默认的执行行为。可以为 XML 中的基本参数提供特定的值,就像其它任何操作一样。例如,可以指定自定义图标:
<action id="open" type="picker_open" icon="USER"/>
要自定义执行行为,应该订阅操作的 ActionPerformedEvent
。如果提供了自己的操作监听器,则所有标准操作都不会执行。这意味着 ActionPerformedEvent
处理器已经覆盖了默认的操作行为。
例如,以下代码覆盖了默认的 picker_lookup
操作行为,使用以模式对话框模式打开的特定界面选择 User
实体:
public class CustomerBrowse extends StandardLookup<Customer> {
@Inject
private LookupScreens lookupScreens;
@Inject
private PickerField<User> userPickerField;
@Subscribe("userPickerField.lookup")
protected void onUserPickerFieldLookupActionPerformed(Action.ActionPerformedEvent event) {
lookupScreens.builder(User.class, this)
.withField(userPickerField)
.withScreen("sec$User.browse") // specific lookup screen
.withLaunchMode(OpenMode.DIALOG) // open as modal dialog
.build()
.show();
}
有关更多详细信息,请参阅 ScreenBuilders bean 描述。
3.5.4.3. 基础操作
在创建自定义操作类时,应该实现 actionPerform()
方法并将操作标识符传递给 BaseAction
构造函数。可以重写任何属性的 getter 方法: getCaption()
、 getDescription()
、 getIcon()
、 getShortcut()
、 isEnabled()
、 isVisible()
、 isPrimary()
。除了 getCaption()
方法之外,这些方法的标准实现返回由 setter 方法设置的值。如果操作名称未通过 setCaption()
方法显式设置,则它使用操作标识符作为键从与操作类包对应的本地化消息包中检索消息。如果没有带有这种键的消息,则返回键本身,即操作标识符。
或者,可以使用流式 API 设置属性并提供 lambda 表达式来处理操作:请参阅 withXYZ()
方法。
BaseAction
可以根据用户权限和当前上下文更改其 enabled
和 visible
属性。
如果满足以下条件,则 BaseAction
是可见的:
-
setVisible(false)
方法没有被调用; -
此操作没有
hide
UI 权限;
如果满足以下条件,则该操作被启用:
-
setEnabled(false)
方法没有被调用; -
此操作没有
hide
或只读 UI 权限; -
isPermitted()
方法返回 true。 -
isApplicable()
方法返回 true。
用法示例:
-
Button 操作:
@Inject private Notifications notifications; @Inject private Button helloBtn; @Subscribe protected void onInit(InitEvent event) { helloBtn.setAction(new BaseAction("hello") { @Override public boolean isPrimary() { return true; } @Override public void actionPerform(Component component) { notifications.create() .withCaption("Hello!") .withType(Notifications.NotificationType.TRAY) .show(); } }); // OR helloBtn.setAction(new BaseAction("hello") .withPrimary(true) .withHandler(e -> notifications.create() .withCaption("Hello!") .withType(Notifications.NotificationType.TRAY) .show())); }
在这个例子中,
helloBtn
按钮标题将被设置为位于消息包中的带有hello
键的字符串。可以重写getCaption()
操作方法以不同的方式初始化按钮名称。 -
以编程方式创建PickerField的操作:
@Inject private UiComponents uiComponents; @Inject private Notifications notifications; @Inject private MessageBundle messageBundle; @Inject private HBoxLayout box; @Subscribe protected void onInit(InitEvent event) { PickerField pickerField = uiComponents.create(PickerField.NAME); pickerField.addAction(new BaseAction("hello") { @Override public String getCaption() { return null; } @Override public String getDescription() { return messageBundle.getMessage("helloDescription"); } @Override public String getIcon() { return "icons/hello.png"; } @Override public void actionPerform(Component component) { notifications.create() .withCaption("Hello!") .withType(Notifications.NotificationType.TRAY) .show(); } }); // OR pickerField.addAction(new BaseAction("hello") .withCaption(null) .withDescription(messageBundle.getMessage("helloDescription")) .withIcon("icons/ok.png") .withHandler(e -> notifications.create() .withCaption("Hello!") .withType(Notifications.NotificationType.TRAY) .show())); box.add(pickerField); }
在此示例中,匿名
BaseAction
派生类用于设置选择器字段按钮的操作。不显示按钮标题,而是在光标悬停在按钮时弹出的带有描述的图标。 -
Table操作:
@Inject private Notifications notifications; @Inject private Table<Customer> table; @Inject private Security security; @Subscribe protected void onInit(InitEvent event) { table.addAction(new HelloAction()); } private class HelloAction extends BaseAction { public HelloAction() { super("hello"); } @Override public void actionPerform(Component component) { notifications.create() .withCaption("Hello " + table.getSingleSelected()) .withType(Notifications.NotificationType.TRAY) .show(); } @Override protected boolean isPermitted() { return security.isSpecificPermitted("myapp.allow-greeting"); } @Override public boolean isApplicable() { return table != null && table.getSelected().size() == 1; } }
在此示例中,声明了
HelloAction
类,它的实例被添加到表格的操作列表中。对具有myapp.allow-greeting
安全权限的用户启用该操作且仅在只选择了表格中的一行时启用。后面这个选中一行启用能有效,是因为 BaseAction 的target
属性会在操作添加到ListComponent
的继承者(Table
或者Tree
)时被自动设置。 -
如果需要一个在选择一行或多行时启用的操作,请使用 BaseAction 的子类 -
ItemTrackingAction
,它添加了isApplicable()
方法的默认实现:@Inject private Table table; @Inject private Notifications notifications; @Subscribe protected void onInit(InitEvent event) { table.addAction(new ItemTrackingAction("hello") { @Override public void actionPerform(Component component) { notifications.create() .withCaption("Hello " + table.getSelected().iterator().next()) .withType(Notifications.NotificationType.TRAY) .show(); } }); }
3.5.5. 对话框消息
Dialogs
接口设计用来展示标准对话框窗口。其 createMessageDialog()
和 createOptionDialog()
方法是流式 API 的入口点,可以用来创建和显示对话框。
下面的例子中,当用户点击按钮时,会显示一个消息对话框:
@Inject
private Dialogs dialogs;
@Subscribe("showDialogBtn")
protected void onShowDialogBtnClick(Button.ClickEvent event) {
dialogs.createMessageDialog().withCaption("Information").withMessage("Message").show();
}
可以在消息中使用 \n
字符来换行。如果要显示 HTML,可以用 withContentMode()
方法带 ContentMode.HTML
参数。当使用 HTML 时,别忘了转移数据内容以防恶意代码注入。
使用下面的方法可以自定义消息对话框的外观和行为:
-
withModal()
- 如果使用false
,对话框不以模态窗展示,此时用户可以与应用程序的其他部分交互。 -
withCloseOnClickOutside()
- 如果使用true
,并且窗口是模态展示时,用户可以点击对话框之外的地方来关闭对话框。 -
withWidth()
,withHeight()
可以设置需要的弹窗大小。
示例:
@Inject
private Dialogs dialogs;
@Subscribe("showDialogBtn")
protected void onShowDialogBtnClick(Button.ClickEvent event) {
dialogs.createMessageDialog()
.withCaption("Information")
.withMessage("<i>Message<i/>")
.withContentMode(ContentMode.HTML)
.withCloseOnClickOutside(true)
.withWidth("100px")
.withHeight("300px")
.show();
}
选项对话框展示了一个消息和一组用户交互的按钮。使用 withActions()
方法可以提供操作,每个操作在对话框中以按钮的形式展示。示例:
@Inject
private Dialogs dialogs;
@Subscribe("showDialogBtn")
protected void onShowDialogBtnClick(Button.ClickEvent event) {
dialogs.createOptionDialog()
.withCaption("Confirm")
.withMessage("Are you sure?")
.withActions(
new DialogAction(DialogAction.Type.YES, Action.Status.PRIMARY).withHandler(e -> {
doSomething();
}),
new DialogAction(DialogAction.Type.NO)
)
.show();
}
当按钮被点击时,对话框会关闭并且调用相应操作的 actionPerform()
方法。
DialogAction
基类设计用来创建带有标准名称和图标的操作。支持五种使用 DialogAction.Type
枚举定义的操作类型:OK
,CANCEL
,YES
,NO
,CLOSE
。对应的按钮名称通过主语言消息包获取。
DialogAction
构造器的第二个参数用来为操作的按钮设置特殊的可视化样式。c-primary-action
样式提供的 Status.PRIMARY
会高亮对应的按钮并使得它被选中。如果对话框中有多个操作使用了 Status.PRIMARY
,只有第一个操作的按钮能使用正确的样式和图标。
3.5.6. 通知消息
通知消息是显示在主应用程序窗口中间或者角落的弹窗。这些弹窗能自动消失,也可以在用户点击界面或按下 Esc 时消失。
要显示通知消息,可以在界面控制器中注入 Notifications
bean 然后使用其流式接口。在下面的例子中,点击按钮会显示通知消息:
@Inject
private Notifications notifications;
@Subscribe("sayHelloBtn")
protected void onSayHelloBtnClick(Button.ClickEvent event) {
notifications.create().withCaption("Hello!").show();
}
一个通知消息可以有一条描述,描述使用更轻的字体展示在标题下面:
@Inject
private Notifications notifications;
@Subscribe("sayHelloBtn")
protected void onSayHelloBtnClick(Button.ClickEvent event) {
notifications.create().withCaption("Greeting").withDescription("Hello World!").show();
}
通知消息有以下类型:
-
TRAY
- 显示在应用程序右下角的消息,会自动消失。 -
HUMANIZED
– 显示在界面中间的标准消息,会自动消失。 -
WARNING
– 警告消息。当用户点击界面时消失。 -
ERROR
– 错误消息。当用户点击界面时消失。
默认类型是 HUMANIZED
。也可以在 create()
方法中使用其他类型:
@Inject
private Notifications notifications;
@Subscribe("sayHelloBtn")
protected void onSayHelloBtnClick(Button.ClickEvent event) {
notifications.create(Notifications.NotificationType.TRAY).withCaption("Hello World!").show();
}
可以在消息中使用 \n
来换行。如果需要显示 HTML,可以用 withContentMode()
方法:
@Inject
private Notifications notifications;
@Subscribe("sayHelloBtn")
protected void onSayHelloBtnClick(Button.ClickEvent event) {
notifications.create()
.withContentMode(ContentMode.HTML)
.withCaption("<i>Hello World!</i>")
.show();
}
当使用 HTML 时,别忘了对数据进行转义,以防恶意代码注入。
其他诸如 withHideDelayMs()
,withPosition()
和 withStyleName()
的方法可以用来自定义通知消息的外观和行为。
3.5.7. 后台任务
后台任务机制用于在客户端层异步执行任务,不阻塞用户界面。
要使用后台任务,请执行以下操作:
-
定义一个继承自
BackgroundTask
抽象类的任务。将界面控制器的引用传递给任务的构造器,该控制器将与任务和任务超时关联起来。关闭界面将中断与其相关的任务。此外,任务将在指定的超时后自动中断。
任务执行的实际操作在run()方法中实现。
-
通过将任务实例传递给
BackgroundWorker
bean 的handle()
方法,创建一个控制任务的BackgroundTaskHandler
类。可以通过在界面控制器中注入或通过AppBeans
类来获得对BackgroundWorker
的引用。 -
通过调用
BackgroundTaskHandler
的execute()
方法来运行任务。
UI 组件的状态和数据源不能在 |
示例:
@Inject
protected BackgroundWorker backgroundWorker;
@Override
public void init(Map<String, Object> params) {
// Create task with 10 sec timeout and this screen as owner
BackgroundTask<Integer, Void> task = new BackgroundTask<Integer, Void>(10, this) {
@Override
public Void run(TaskLifeCycle<Integer> taskLifeCycle) throws Exception {
// Do something in background thread
for (int i = 0; i < 5; i++) {
TimeUnit.SECONDS.sleep(1); // time consuming computations
taskLifeCycle.publish(i); // publish current progress to show it in progress() method
}
return null;
}
@Override
public void canceled() {
// Do something in UI thread if the task is canceled
}
@Override
public void done(Void result) {
// Do something in UI thread when the task is done
}
@Override
public void progress(List<Integer> changes) {
// Show current progress in UI thread
}
};
// Get task handler object and run the task
BackgroundTaskHandler taskHandler = backgroundWorker.handle(task);
taskHandler.execute();
}
JavaDocs 中提供了 BackgroundTask
、TaskLifeCycle
、BackgroundTaskHandler
类的有关方法的详细信息。
请注意以下事项:
-
BackgroundTask<T, V>
是一个参数化类:-
T
− 显示任务进度的对象类型。在工作线程中调用TaskLifeCycle.publish()
期间,将此类型对象传递给任务的progress()
方法。 -
V
− 任务结果类型被传递给done()
方法。它也可以通过调用BackgroundTaskHandler.getResult()
方法获得,该方法将等待任务完成。
-
-
canceled()
方法只在受控的任务取消时被调用,即在TaskHandler
中调用cancel()
时。 -
handleTimeoutException()
方法在任务超时时被调用。如果正在运行任务的窗口关闭,则任务将在没有通知的情况下停止。
-
任务的
run()
方法应该支持外部中断。要确保这一点,建议在长时间运行的处理中定期检查TaskLifeCycle.isInterrupted()
标识,并在需要时停止执行。另外,不应该静默地忽略掉InterruptedException
(或任何其它异常) - 而应该正确退出方法或根本不处理异常(将异常暴露给调用方)。-
isCancelled()
如果通过调用cancel()
方法中断任务,则此方法返回true
。public String run(TaskLifeCycle<Integer> taskLifeCycle) { for (int i = 0; i < 9_000_000; i++) { if (taskLifeCycle.isCancelled()) { log.info(" >>> Task was cancelled"); break; } else { log.info(" >>> Task is working: iteration #" + i); } } return "Done"; }
-
-
BackgroundTask
对象是无状态的。如果在实现任务类时没有为临时数据创建字段,则可以使用单个任务实例启动多个并行进程。 -
BackgroundHandler
对象(它的execute()
方法)只能被启动一次。如果需要经常重启任务,请使用BackgroundTaskWrapper
类。 -
使用带有一组静态方法的
BackgroundWorkWindow
类或BackgroundWorkProgressWindow
类来显示带进度指示器和 Cancel 按钮的模式窗口。可以定义进度指示类型,并允许或禁止取消窗口的后台任务。 -
如果需要在任务线程中使用可视化组件的某个值,应该通过
getParams()
方法来获取,该方法在任务启动时在 UI 线程中运行一次。在 run()方法中,可以通过TaskLifeCycle
对象的getParams()
方法访问这些参数。 -
如果发生任何异常,框架将在 UI 线程中调用
BackgroundTask.handleException()
方法,该方法可用于显示错误。 -
后台任务受cuba.backgroundWorker.maxActiveTasksCount和cuba.backgroundWorker.timeoutCheckInterval应用程序属性的影响。
在 Web 客户端中,后台任务是使用 Vaadin 框架提供的 HTTP 推送实现的。有关如何为此技术设置 Web 服务器的信息,请参阅 https://vaadin.com/wiki/-/wiki/Main/Working+around+push+issues 。 |
如果不使用后台任务,但想要从非 UI 线程更新 UI 状态,请使用 |
3.5.8. 主题
主题用于管理应用程序的视觉展现。
一个 Web 主题由 SCSS 文件和其它的资源比如图像组成。
如果希望在多个项目中使用同一个主题,可以在应用程序组件中包含此主题或者创建一个可重用的主题 JAR。
3.5.8.1. 使用现有主题
平台包括三个即用型主题:Hover 、Halo 和 Havana。默认情况下,应用程序将使用cuba.web.theme应用程序属性中指定的主题。
用户可以在标准 Help > Settings 界面中选择其它主题。如果要禁用选择主题的选项,请在项目的web-screens.xml文件中注册 settings
界面,并为其设置 changeThemeEnabled = false
参数:
<screen id="settings" template="/com/haulmont/cuba/web/app/ui/core/settings/settings-window.xml">
<param name="changeThemeEnabled" value="false"/>
</screen>
3.5.8.2. 扩展现有主题
平台主题可以在项目中被修改。在修改后的主题中,可以:
- 文件结构和构建脚本
-
主题在 SCSS 中定义。要修改(扩展)项目中的主题,应该在 web 模块中创建特定的文件结构。
一种便捷的方法是使用 CUBA Studio:在主菜单中,单击 CUBA > Advanced > Manage themes > Create theme extension。在弹出窗口中选择要扩展的主题。另一种方法是使用 CUBA CLI 中的
theme
命令。最终,以下目录结构将在
modules/web
目录中被创建(对于 Halo 主题扩展):themes/ halo/ branding/ app-icon-login.png app-icon-menu.png com.company.application/ app-component.scss halo-ext.scss halo-ext-defaults.scss favicon.ico styles.scss
除此之外,build.gradle脚本将会添加进
buildScssThemes
任务,该任务在每次构建 web 模块时自动执行。可选的deployThemes任务可用于将主题中的更改快速应用于正在运行的应用程序。如果项目包含带有扩展主题的应用程序组件,并且希望此扩展用于整个项目,那么也应该为项目创建主题扩展。有关如何继承组件主题的详细信息,请参阅使用应用程序组件中的主题部分。
- 更改品牌
-
可以配置一些品牌相关的属性,例如图标、登录和主应用程序窗口标题以及网站图标(
favicon.ico
)。要使用自定义图片,请替换
modules/web/themes/halo/branding
目录中的默认图片。要设置窗口标题和登录窗口欢迎文本,请在 web 模块的主消息包中设置窗口标题和登录窗口欢迎文本(即
modules/web/<root_package>/web/messages.properties
文件及其针对不同语言环境的变体)。消息包允许为不同的用户区域设置使用不同的图像文件。示例messages.properties
文件:application.caption = MyApp application.logoImage = branding/myapp-menu.png loginWindow.caption = MyApp Login loginWindow.welcomeLabel = Welcome to MyApp! loginWindow.logoImage = branding/myapp-login.png
favicon.ico
的路径没有被指定,因为它必须位于主题的根目录中。
- 添加字体
-
可以为 Web 主题添加自定义字体。添加一个 Font Family,将其导入
styles.scss
文件的第一行,例如:@import url(http://fonts.googleapis.com/css?family=Roboto);
- 创建新样式
-
为显示客户名称的字段设置黄色背景颜色的示例。
在 XML 描述中, 定义了 FieldGroup组件:
<fieldGroup id="fieldGroup" datasource="customerDs"> <field property="name"/> <field property="address"/> </fieldGroup>
FieldGroup
的field
元素没有stylename属性,因此我们必须在控制器中设置字段的样式名称:@Named("fieldGroup.name") private TextField nameField; @Override public void init(Map<String, Object> params) { nameField.setStyleName("name-field"); }
在
halo-ext.scss
文件中,将新样式定义添加到halo-ext
mixin:@mixin com_company_application-halo-ext { .name-field { background-color: lightyellow; } }
重建项目后,字段将如下所示:
- 修改可视化组件的现有样式
-
要修改现有组件的样式参数,请将相应的 CSS 代码添加到
halo-ext.scss
文件的halo-ext
mixin 中。使用 Web 浏览器的开发人员工具查找分配给可视化组件元素的 CSS 类。例如,要以粗体显示应用程序菜单项,halo-ext.scss
文件的内容应如下所示:@mixin com_company_application-halo-ext { .v-menubar-menuitem-caption { font-weight: bold; } }
- 修改通用参数
-
主题包含许多控制应用程序背景颜色、组件大小、边距和其它参数的 SCSS 变量。
以下是 Halo 主题扩展的示例,因为它基于来自 Vaadin 的 Valo 主题,并提供最广泛的自定义选项。
themes/halo/halo-ext-defaults.scss
文件用于覆盖主题变量。大多数 Halo 变量对应于 Valo 文档 中描述的变量。以下是最常见的变量:$v-background-color: #fafafa; /* component background colour */ $v-app-background-color: #e7ebf2; /* application background colour */ $v-panel-background-color: #fff; /* panel background colour */ $v-focus-color: #3b5998; /* focused element colour */ $v-error-indicator-color: #ed473b; /* empty required fields colour */ $v-line-height: 1.35; /* line height */ $v-font-size: 14px; /* font size */ $v-font-weight: 400; /* font weight */ $v-unit-size: 30px; /* base theme size, defines the height for buttons, fields and other elements */ $v-font-size--h1: 24px; /* h1-style Label size */ $v-font-size--h2: 20px; /* h2-style Label size */ $v-font-size--h3: 16px; /* h3-style Label size */ /* margins for containers */ $v-layout-margin-top: 10px; $v-layout-margin-left: 10px; $v-layout-margin-right: 10px; $v-layout-margin-bottom: 10px; /* spacing between components in a container (if enabled) */ $v-layout-spacing-vertical: 10px; $v-layout-spacing-horizontal: 10px; /* whether filter search button should have "friendly" style*/ $cuba-filter-friendly-search-button: true; /* whether button that has primary action or marked as primary itself should be highlighted*/ $cuba-highlight-primary-action: false; /* basic table and datagrid settings */ $v-table-row-height: 30px; $v-table-header-font-size: 13px; $v-table-cell-padding-horizontal: 7px; $v-grid-row-height $v-grid-row-selected-background-color $v-grid-cell-padding-horizontal /* input field focus style */ $v-focus-style: inset 0px 0px 5px 1px rgba($v-focus-color, 0.5); /* required fields focus style */ $v-error-focus-style: inset 0px 0px 5px 1px rgba($v-error-indicator-color, 0.5); /* animation for elements is enabled by default */ $v-animations-enabled: true; /* popup window animation is disabled by default */ $v-window-animations-enabled: false; /* inverse header is controlled by cuba.web.useInverseHeader property */ $v-support-inverse-menu: true; /* show "required" indicators for components */ $v-show-required-indicators: false !default;
下面提供了具有深色背景和略微减少外边距的示例主题
halo-ext-defaults.scss
:$v-background-color: #444D50; $v-font-size--h1: 22px; $v-font-size--h2: 18px; $v-font-size--h3: 16px; $v-layout-margin-top: 8px; $v-layout-margin-left: 8px; $v-layout-margin-right: 8px; $v-layout-margin-bottom: 8px; $v-layout-spacing-vertical: 8px; $v-layout-spacing-horizontal: 8px; $v-table-row-height: 25px; $v-table-header-font-size: 13px; $v-table-cell-padding-horizontal: 5px; $v-support-inverse-menu: false;
另外一个示例展示了使用一组变量使得 Halo 主题看上去跟旧的 Havana 主题差不多,Havana 主题从框架 7.0 版本开始已经移除了。
$cuba-menubar-background-color: #315379; $cuba-menubar-border-color: #315379; $v-table-row-height: 25px; $v-selection-color: rgb(77, 122, 178); $v-table-header-font-size: 12px; $v-textfield-border: 1px solid #A5C4E0; $v-selection-item-selection-color: #4D7AB2; $v-app-background-color: #E3EAF1; $v-font-size: 12px; $v-font-weight: 400; $v-unit-size: 25px; $v-border-radius: 0px; $v-border: 1px solid #9BB3D3 !default; $v-font-family: Verdana,tahoma,arial,geneva,helvetica,sans-serif,"Trebuchet MS"; $v-panel-background-color: #ffffff; $v-background-color: #ffffff; $cuba-menubar-menuitem-text-color: #ffffff; $cuba-app-menubar-padding-top: 8px; $cuba-app-menubar-padding-bottom: 8px; $cuba-menubar-text-color: #ffffff; $cuba-menubar-submenu-padding: 1px;
- 更改应用程序标题
-
Halo 主题支持cuba.web.useInverseHeader属性,该属性控制应用程序标题的颜色。默认情况下,此属性设置为
true
,它设置一个暗色(高对比)标题。只需将此属性设置为false
,即可不需要对主题进行任何更改而创建一个亮色标题。
3.5.8.3. 创建自定义主题
可以在项目中创建一个或多个应用程序主题,并为用户提供选择最合适的应用程序主题的时机。创建新主题还允许覆盖 *-theme.properties 文件
中的变量,这些变量定义了一些服务端参数:
-
默认对话框窗口大小。
-
默认输入框宽度。
-
某些组件的尺寸(Filter 、FileMultiUploadField)。
-
如果 cuba.web.useFontIcons 属性启用,则在标准操作和平台界面中使用 Font Awesome 图标时,图标名称和
com.vaadin.server.FontAwesome
枚举的常量值对应。
可以在 CUBA Studio 、 CUBA CLI 中轻松创地建新主题,也可以手动创建。我们看看以 Hover Dark 自定义主题为例的所有三种创建方式。
- 在 CUBA Studio 中创建:
-
-
在主菜单中,单击 CUBA > Advanced > Manage themes > Create custom theme。输入新主题的名称: hover-dark。在 Base theme 下拉列表中选择 hover 主题。
将在 web 模块中创建所需的文件结构。
webThemesModule
模块及其配置将自动被添加到settings.gradle
和build.gradle文件中。此外,生成的deployThemes
gradle 任务允许在不重启服务器的情况下查看主题更改。
-
- 手动创建:
-
-
在项目的 web 模块中创建以下文件结构:
web/ src/ themes/ hover-dark/ branding/ app-icon-login.png app-icon-menu.png com.haulmont.cuba/ app-component.scss favicon.ico hover-dark.scss hover-dark-defaults.scss styles.scss
-
app-component.scss
文件:@import "../hover-dark"; @mixin com_haulmont_cuba { @include hover-dark; }
-
hover-dark.scss
文件:@import "../hover/hover"; @mixin hover-dark { @include hover; }
-
styles.scss
文件:@import "hover-dark-defaults"; @import "hover-dark"; .hover-dark { @include hover-dark; }
-
在 web 模块的 web 子目录中创建
hover-dark-theme.properties
文件:@include=hover-theme.properties
-
将
webThemesModule
模块添加到settings.gradle
文件中:include(":${modulePrefix}-global", ":${modulePrefix}-core", ":${modulePrefix}-web", ":${modulePrefix}-web-themes") //... project(":${modulePrefix}-web-themes").projectDir = new File(settingsDir, 'modules/web/themes')
-
将
webThemesModule
模块配置添加到build.gradle文件中:def webThemesModule = project(":${modulePrefix}-web-themes") configure(webThemesModule) { apply(plugin: 'java') apply(plugin: 'maven') apply(plugin: 'cuba') appModuleType = 'web-themes' buildDir = file('../build/scss-themes') sourceSets { main { java { srcDir '.' } resources { srcDir '.' } } } }
-
最后,在
build.gradle
中创建deployThemes
gradle 任务,以查看更改而不重启服务器:configure(webModule) { // . . . task buildScssThemes(type: CubaWebScssThemeCreation) task deployThemes(type: CubaDeployThemeTask, dependsOn: buildScssThemes) assemble.dependsOn buildScssThemes }
-
- 在 CUBA CLI中创建:
-
-
运行
theme
命令,然后选择 hover 主题。将在项目的 web 模块中创建特定的文件结构。
-
修改生成的文件结构和文件内容,使其与上面的文件相对应。
-
在 web 模块的资源目录中创建
hover-dark-theme.properties
文件:@include=hover-theme.properties
CLI 将自动更新
build.gradle
和settings.gradle
文件。 -
另请参阅创建 Facebook 主题部分中的示例。
- 修改服务端主题参数
-
在 Halo 主题中,标准操作和平台界面会默认使用 Font Awesome 图标(如果启用了cuba.web.useFontIcons)。在这种情况下,可以通过在
<your_theme>-theme.properties
文件中设置图标和字体元素名称之间所需的映射来替换标准图标。例如,要在新 Facebook 主题中要为create
操作使用"plus"图标,facebook-theme.properties
文件应包含以下内容:@include=halo-theme.properties cuba.web.icons.create.png = font-icon:PLUS
Facebook 主题中带有修改后的
create
操作的标准用户浏览界面的片段:
3.5.8.3.1. 创建 Hover Dark 主题
这里介绍创建 Hover Dark 主题的步骤,这个主题是默认 Hover 主题的暗色变体。使用此主题的示例应用程序可在 GitHub 上找到。
-
按照创建自定义主题部分中的说明在项目中创建新的 hover-dark 主题。
会在 web 模块中创建所需的文件结构。新建的
webThemesModule
模块及其配置将自动被添加到settings.gradle
和 build.gradle 文件中。 -
重新设置
hover-dark-defaults.scss
文件中的默认样式变量,比如,可以用以下变量值替换其中的变量:@import "../hover/hover-defaults"; $v-app-background-color: #262626; $v-background-color: lighten($v-app-background-color, 12%); $v-border: 1px solid (v-tint 0.8); $font-color: valo-font-color($v-background-color, 0.85); $v-button-font-color: $font-color; $v-font-color: $font-color; $v-link-font-color: lighten($v-focus-color, 15%); $v-link-text-decoration: none; $v-textfield-background-color: $v-background-color; $cuba-hover-color: #75a4c1; $cuba-maintabsheet-tabcontainer-background-color: $v-app-background-color; $cuba-menubar-background-color: lighten($v-app-background-color, 4%); $cuba-tabsheet-tab-caption-selected-color: $v-font-color; $cuba-window-modal-header-background: $v-background-color; $cuba-menubar-menuitem-border-radius: 0;
-
使用 cuba.themeConfig 应用程序属性定义要在应用程序中使用的主题:
cuba.themeConfig = com/haulmont/cuba/hover-theme.properties /com/company/demo/web/hover-dark-theme.properties
于是,在应用程序中将有两个主题将可用:默认 Hover 主题及其暗色变体。
3.5.8.3.2. 创建 Facebook 主题
以下是创建基于 Halo 的 Facebook 主题的示例,该主题风格类似流行的社交网络界面。
-
在 CUBA Studio 中,点击 CUBA > Advanced > Manage themes > Create custom theme。设置主题名称 -
facebook
,选择halo
作为基础主题,然后单击 Create。在项目中将创建新的主题目录:themes/ facebook/ branding/ app-icon-login.png app-icon-menu.png com.haulmont.cuba/ app-component.scss // cuba app-component include facebook.scss // main theme file facebook-defaults.scss // main theme variables favicon.ico styles.scss // entry point of SCSS build procedure
styles.scss
文件包含主题列表:@import "facebook-defaults"; @import "facebook"; .facebook { @include facebook; }
facebook.scss
文件:@import "../halo/halo"; @mixin facebook { @include halo; }
com.haulmont.cuba
中的app-component.scss
文件:@import "../facebook"; @mixin com_haulmont_cuba { @include facebook; }
-
修改
facebook-defaults.scss
中的主题变量。可以通过在 Studio 中单击 Manage themes > Edit Facebook theme variables 或在 IDE 中执行此操作:@import "../halo/halo-defaults"; $v-background-color: #fafafa; $v-app-background-color: #e7ebf2; $v-panel-background-color: #fff; $v-focus-color: #3b5998; $v-border-radius: 0; $v-textfield-border-radius: 0; $v-font-family: Helvetica, Arial, 'lucida grande', tahoma, verdana, arial, sans-serif; $v-font-size: 14px; $v-font-color: #37404E; $v-font-weight: 400; $v-link-text-decoration: none; $v-shadow: 0 1px 0 (v-shade 0.2); $v-bevel: inset 0 1px 0 v-tint; $v-unit-size: 30px; $v-gradient: v-linear 12%; $v-overlay-shadow: 0 3px 8px v-shade, 0 0 0 1px (v-shade 0.7); $v-shadow-opacity: 20%; $v-selection-overlay-padding-horizontal: 0; $v-selection-overlay-padding-vertical: 6px; $v-selection-item-border-radius: 0; $v-line-height: 1.35; $v-font-size: 14px; $v-font-weight: 400; $v-unit-size: 25px; $v-font-size--h1: 22px; $v-font-size--h2: 18px; $v-font-size--h3: 16px; $v-layout-margin-top: 8px; $v-layout-margin-left: 8px; $v-layout-margin-right: 8px; $v-layout-margin-bottom: 8px; $v-layout-spacing-vertical: 8px; $v-layout-spacing-horizontal: 8px; $v-table-row-height: 25px; $v-table-header-font-size: 13px; $v-table-cell-padding-horizontal: 5px; $v-focus-style: inset 0px 0px 1px 1px rgba($v-focus-color, 0.5); $v-error-focus-style: inset 0px 0px 1px 1px rgba($v-error-indicator-color, 0.5);
-
web 模块的
src
目录中的facebook-theme.properties
文件可用于覆盖服务端使用的平台的halo-theme.properties
文件中的主题变量。 -
新主题已被自动添加到
web-app.properties
文件中:cuba.web.theme = facebook cuba.themeConfig = com/haulmont/cuba/halo-theme.properties /com/company/application/web/facebook-theme.properties
cuba.themeConfig 属性定义了应用程序的 Settings 菜单中可供用户使用的主题。
重新构建应用程序并启动服务。现在,用户将在首次登录时看到使用 Facebook 主题的应用程序,并且可以在 Help > Settings 菜单中选择 Facebook、Halo 和 Havana 主题。
3.5.8.4. 使用应用程序组件中的主题
如果项目包含带有自定义主题的应用程序组件,则可以将此主题用于整个项目。
要按原样继承主题,只需将其添加到cuba.themeConfig应用程序属性:
cuba.web.theme = {theme-name}
cuba.themeConfig = com/haulmont/cuba/hover-theme.properties /com/company/{app-component-name}/{theme-name}-theme.properties
如果要覆盖父主题中的某些变量,则首先需要在项目中创建主题扩展。
在下面的例子中,我们将使用创建自定义主题部分中的 facebook
主题。
-
按照步骤为应用程序组件创建
facebook
主题。 -
使用 Studio 菜单安装应用程序组件,如应用程序组件示例部分所述。
-
在使用应用程序组件的项目中扩展
halo
主题。 -
通过 IDE,将
themes
目录下的所有文件名中的halo
重命名为facebook
,以获得以下结构:themes/ facebook/ branding/ app-icon-login.png app-icon-menu.png com.company.application/ app-component.scss facebook-ext.scss facebook-ext-defaults.scss favicon.ico styles.scss
-
app-component.scss
文件合并应用程序组件的主题修改。在 SCSS 构建过程中,Gradle 插件会自动查找应用程序组件并将其导入生成的modules/web/build/themes-tmp/VAADIN/themes/{theme-name}/app-components.scss
文件中。默认情况下,
app-component.scss
不包含来自{theme-name}-ext-defaults
的变量改动。要包含变量改动到 app 组件包,需要在app-component.scss
中手动导入:@import "facebook-ext"; @import "facebook-ext-defaults"; @mixin com_company_application { @include com_company_application-facebook-ext; }
在这个阶段,
facebook
主题已经从 app 组件导入到项目中。 -
现在,可以使用
com.company.application
包中的facebook-ext.scss
和facebook-ext-defaults.scss
文件覆盖 app 组件主题中的变量,并为具体项目自定义变量。 -
将以下属性添加到
web-app.properties
文件中,以使应用程序的 Settings 菜单中的facebook
主题可用。使用相对路径从 app 组件引用facebook-theme.properties
。cuba.web.theme = facebook cuba.themeConfig = com/haulmont/cuba/hover-theme.properties /com/company/{app-component-name}/facebook-theme.properties
如果主题构建有任何问题,请检查 |
3.5.8.5. 创建可复用主题
任何主题都可以在没有应用程序组件的情况下打包和重用。要创建主题包,需要从头开始创建 Java 项目并将其打包在单个 JAR 文件中。按照以下步骤创建前面示例中 facebook
主题的发布。
-
在 IDE 中使用以下结构创建新项目。它是一个简单的 Java 项目,由 SCSS 文件和主题属性组成:
halo-facebook/ src/ //sources root halo-facebook/ com.haulmont.cuba/ app-component.scss halo-facebook.scss halo-facebook-defaults.scss halo-facebook-theme.properties styles.scss
此示例主题项目可以从 GitHub 下载。
-
build.gradle
脚本:allprojects { group = 'com.haulmont.theme' version = '0.1' } apply(plugin: 'java') apply(plugin: 'maven') sourceSets { main { java { srcDir 'src' } resources { srcDir 'src' } } }
-
settings.gradle
文件:rootProject.name = 'halo-facebook'
-
app-component.scss
文件:@import "../halo-facebook"; @mixin com_haulmont_cuba { @include halo-facebook; }
-
halo-facebook.scss
文件:@import "../@import "../"; @mixin halo-facebook { @include halo; }
-
halo-facebook-defaults.scss
文件:@import "../halo/halo-defaults"; $v-background-color: #fafafa; $v-app-background-color: #e7ebf2; $v-panel-background-color: #fff; $v-focus-color: #3b5998; $v-border-radius: 0; $v-textfield-border-radius: 0; $v-font-family: Helvetica, Arial, 'lucida grande', tahoma, verdana, arial, sans-serif; $v-font-size: 14px; $v-font-color: #37404E; $v-font-weight: 400; $v-link-text-decoration: none; $v-shadow: 0 1px 0 (v-shade 0.2); $v-bevel: inset 0 1px 0 v-tint; $v-unit-size: 30px; $v-gradient: v-linear 12%; $v-overlay-shadow: 0 3px 8px v-shade, 0 0 0 1px (v-shade 0.7); $v-shadow-opacity: 20%; $v-selection-overlay-padding-horizontal: 0; $v-selection-overlay-padding-vertical: 6px; $v-selection-item-border-radius: 0; $v-line-height: 1.35; $v-font-size: 14px; $v-font-weight: 400; $v-unit-size: 25px; $v-font-size--h1: 22px; $v-font-size--h2: 18px; $v-font-size--h3: 16px; $v-layout-margin-top: 8px; $v-layout-margin-left: 8px; $v-layout-margin-right: 8px; $v-layout-margin-bottom: 8px; $v-layout-spacing-vertical: 8px; $v-layout-spacing-horizontal: 8px; $v-table-row-height: 25px; $v-table-header-font-size: 13px; $v-table-cell-padding-horizontal: 5px; $v-focus-style: inset 0px 0px 1px 1px rgba($v-focus-color, 0.5); $v-error-focus-style: inset 0px 0px 1px 1px rgba($v-error-indicator-color, 0.5); $v-show-required-indicators: true;
-
halo-facebook-theme.properties
文件:@include=halo-theme.properties
-
-
使用 Gradle 任务构建和安装项目:
gradle assemble install
-
通过修改
build.gradle
文件,将主题作为 Maven 依赖项添加到基于 CUBA 的项目中,有两种配置方式(gradle configurations):themes 和 compile:configure(webModule) { //... dependencies { provided(servletApi) compile(guiModule) compile('com.haulmont.theme:halo-facebook:0.1') themes('com.haulmont.theme:halo-facebook:0.1') } //... }
如果在本地安装主题,不要忘记将
mavenLocal()
添加到仓库列表中:打开 Studio 中的 Project Properties 部分,并将本地 Maven 仓库坐标添加到仓库列表中。 -
要在项目中继承此主题并修改它,必须扩展此主题。扩展
halo
主题并将themes/halo
文件夹重命名为themes/halo-facebook
:themes/ halo-facebook/ branding/ app-icon-login.png app-icon-menu.png com.company.application/ app-component.scss halo-ext.scss halo-ext-defaults.scss favicon.ico styles.scss
-
修改
styles.scss
文件:@import "halo-facebook-defaults"; @import "com.company.application/halo-ext-defaults"; @import "app-components"; @import "com.company.application/halo-ext"; .halo-facebook { // include auto-generated app components SCSS @include app_components; @include com_company_application-halo-ext; }
-
最后一步是在
web-app.properties
文件中定义halo-facebook-theme.properties
文件:cuba.themeConfig = com/haulmont/cuba/hover-theme.properties /halo-facebook/halo-facebook-theme.properties
现在,可以从 Help > Settings 菜单中选择 halo-facebook
主题,或使用 cuba.web.theme
应用程序属性设置默认主题。
3.5.9. 图标
例如,要向 Halo 主题扩展添加图标,必须将图像文件添加到扩展现有主题部分描述的 modules/web/themes/halo
目录中(建议创建子文件夹):
themes/
halo/
icons/
cool-icon.png
在以下章节中,我们将了解如何在可视化组件中使用图标以及如何从任意字体库添加图标。
3.5.9.1. 图标集
图标集允许将可视化组件中图标的使用与主题中图片的实际路径或字体元素常量解耦。它们还简化了对继承自应用程序组件的 UI 中使用的图标的覆盖。
图标集是一个枚举,包含了与图标对应的枚举值。图标集必须实现 Icons.Icon
接口,该接口有一个参数:表示图标资源的字符串,例如,font-icon:CHECK
或 icons/myawesomeicon.png
。要获取资源,请使用平台提供的 Icons
bean。
可以在 web 或者 gui 模块中创建图标集。图标集中所有图标的名称应匹配正则表达式:[A-Z]_
,即它们应仅包含大写字母和下划线。
例如:
public enum MyIcon implements Icons.Icon {
// adding new icon
COOL_ICON("icons/cool-icon.png"),
// overriding a CUBA default icon
OK("icons/my-ok.png");
protected String source;
MyIcon(String source) {
this.source = source;
}
@Override
public String source() {
return source;
}
}
图标集应该在cuba.iconsConfig应用程序属性中注册,例如:
cuba.iconsConfig = +com.company.demo.gui.icons.MyIcon
要使某个应用程序组件中的图标集在目标项目中可用,此属性应该添加到应用程序组件描述。 |
现在,可以在界面 XML 描述中以声明方式使用此图标集中的图标:
<button icon="COOL_ICON"/>
或在界面控制器中以编程方式使用:
button.setIconFromSet(MyIcon.COOL_ICON);
以下前缀允许以声明方式使用不同来源的图标:
-
theme
- 图标将由当前主题目录提供,例如,web/themes/halo/awesomeFolder/superIcon.png
:<button icon="theme:awesomeFolder/superIcon.png"/>
-
file
- 图标将由文件系统提供:<button icon="file:D:/superIcon.png"/>
-
classpath
- 图标将由类路径提供,例如,com/company/demo/web/superIcon.png
<button icon="classpath:/com/company/demo/web/superIcon.png"/>
平台提供了一个预定义的图标集 - CubaIcon
。它包括几乎完整的 FontAwesome
图标集和 CUBA 特定的图标。可以在 Studio 图标编辑界面中选择这些图标:
3.5.9.2. 使用其它字体库中的图标
为了增强主题扩展,可能需要创建图标并将其嵌入到字体中,以及使用一些外部图标库。
-
在 web 模块中,为新图标创建实现
com.vaadin.server.FontIcon
接口的enum
类:import com.vaadin.server.FontIcon; import com.vaadin.server.GenericFontIcon; public enum IcoMoon implements FontIcon { HEADPHONES(0XE900), SPINNER(0XE905); public static final String FONT_FAMILY = "IcoMoon"; private int codepoint; IcoMoon(int codepoint) { this.codepoint = codepoint; } @Override public String getFontFamily() { return FONT_FAMILY; } @Override public int getCodepoint() { return codepoint; } @Override public String getHtml() { return GenericFontIcon.getHtml(FONT_FAMILY, codepoint); } @Override public String getMIMEType() { throw new UnsupportedOperationException(FontIcon.class.getSimpleName() + " should not be used where a MIME type is needed."); } public static IcoMoon fromCodepoint(final int codepoint) { for (IcoMoon f : values()) { if (f.getCodepoint() == codepoint) { return f; } } throw new IllegalArgumentException("Codepoint " + codepoint + " not found in IcoMoon"); } }
-
向主题扩展添加新样式。建议在主题扩展的主文件夹中创建一个特定的子文件夹
fonts
,例如modules/web/themes/halo/com.company.demo/fonts
。将样式和字体文件放在特有的子文件夹中,例如fonts/icomoon
。字体文件由以下扩展名表示:
-
.eot
, -
.svg
, -
.ttf
, -
.woff
.本例中使用了一个开源字体集
icomoon
,由 4 个联合使用的文件组成:icomoon.eot
、icomoon.svg
、icomoon.ttf
、icomoon.woff
。
-
-
创建一个包含
@font-face
样式和一个图标样式 CSS 类的文件。下面是icomoon.scss
文件的示例,其中IcoMoon
css 类名对应于FontIcon#getFontFamily
方法返回的值:@mixin icomoon-style { /* use !important to prevent issues with browser extensions that change fonts */ font-family: 'icomoon' !important; speak: none; font-style: normal; font-weight: normal; font-variant: normal; text-transform: none; line-height: 1; /* Better Font Rendering =========== */ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } @font-face { font-family: 'icomoon'; src:url('icomoon.eot?hwgbks'); src:url('icomoon.eot?hwgbks#iefix') format('embedded-opentype'), url('icomoon.ttf?hwgbks') format('truetype'), url('icomoon.woff?hwgbks') format('woff'), url('icomoon.svg?hwgbks#icomoon') format('svg'); font-weight: normal; font-style: normal; } .IcoMoon { @include icomoon-style; }
-
在
halo-ext.scss
文件或其它主题扩展文件中添加对上述包含字体样式的文件的引用。@import "fonts/icomoon/icomoon";
-
然后创建新的图标集,这是一个实现
Icons.Icon
接口的枚举:import com.haulmont.cuba.gui.icons.Icons; public enum IcoMoonIcon implements Icons.Icon { HEADPHONES("ico-moon:HEADPHONES"), SPINNER("ico-moon:SPINNER"); protected String source; IcoMoonIcon(String source) { this.source = source; } @Override public String source() { return source; } }
-
创建新的
IconProvider
.为了管理自定义图标集,CUBA 框架提供了由
IconProvider
和IconResolver
组成的机制。IconProvider
是一个标记接口,只存在于 web 模块中,可以通过图标路径提供资源(com.vaadin.server.Resource
)。IconResolver
bean 获取实现IconProvider
接口的所有 bean,并遍历它们以找到可以为图标提供资源的 bean。要使用这种机制,应该创建
IconProvider
的实现:import com.haulmont.cuba.web.gui.icons.IconProvider; import com.vaadin.server.Resource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; @Order(10) @Component public class IcoMoonIconProvider implements IconProvider { private final Logger log = LoggerFactory.getLogger(IcoMoonIconProvider.class); @Override public Resource getIconResource(String iconPath) { Resource resource = null; iconPath = iconPath.split(":")[1]; try { resource = ((Resource) IcoMoon.class .getDeclaredField(iconPath) .get(null)); } catch (IllegalAccessException | NoSuchFieldException e) { log.warn("There is no icon with name {} in the FontAwesome icon set", iconPath); } return resource; } @Override public boolean canProvide(String iconPath) { return iconPath.startsWith("ico-moon:"); } }
这里我们用
@Order annotation
注解显式地为这个 bean 指定顺序。 -
在应用程序属性文件中注册自定义图标集:
cuba.iconsConfig = +com.company.demo.gui.icons.IcoMoonIcon
现在,在界面 XML 描述中可以直接引用图标的类和 枚举
元素 :
<button caption="Headphones" icon="ico-moon:HEADPHONES"/>
或者在 Java 控制器中:
spinnerBtn.setIconFromSet("ico-moon:SPINNER");
这样新图标会添加到按钮上:
- 覆盖图标集的图标
-
图标集机制可以覆盖其它图标集中的图标。为此,应该创建并注册一个具有相同图标(枚举值)但不同图标路径(
source
)的新图标集(枚举)。在下面的示例中,创建了一个新的图标枚举MyIcon
,用于覆盖CubaIcon
图标集中的标准图标。-
默认图标集:
public enum CubaIcon implements Icons.Icon { OK("font-icon:CHECK"), CANCEL("font-icon:BAN"), ... }
-
新图标集:
public enum MyIcon implements Icons.Icon { OK("icons/my-custom-ok.png"), ... }
-
在
web-app.properties
中注册的新图标集:cuba.iconsConfig = +com.company.demo.gui.icons.MyIcon
现在,新的 OK 图标将代替标准图标而被使用:
Icons icons = AppBeans.get(Icons.NAME); button.setIcon(icons.getIcon(CubaIcon.OK))
如果需要忽略重新定义的图标,仍然可以通过使用标准图标的路径而不是选项名称来使用标准图标:
<button caption="Created" icon="icons/create.png"/>
或者
button.setIcon(CubaIcon.CREATE_ACTION.source());
-
3.5.10. 可视化组件的 DOM 和 CSS 属性
框架提供了设置组件原生 HTML 属性的 API,用于为可视化组件设置 DOM 和 CSS 属性。
HtmlAttributes
bean 允许使用以下方法通过编程方式设置 DOM/CSS 属性:
-
setDomAttribute()
- 在 UI 组件的最顶层 HTML 元素上设置 DOM 属性。接受组件的标识符、DOM 属性名称(例如title
)和属性值作为参数。 -
setCssProperty()
- 在 UI 组件的最顶层 HTML 元素上设置 CSS 属性值。接受组件的标识符、CSS 属性名称(例如border-color
)和属性值作为参数。
最常见的 DOM 属性名称和 CSS 属性名称在 HtmlAttributes
bean 类中作为常量提供,但也可以使用任何自定义属性。
特定属性的功能可能会根据应用此属性的组件而有所不同。某些可视化组件可能为了特殊的目的隐式使用了相同的属性,因此上述方法在某些情况下可能不起作用。 |
HtmlAttributes
bean 应该注入界面控制器中并按如下方式使用:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd"
caption="Demo"
messagesPack="com.company.demo.web">
<layout>
<button id="demoButton"
caption="msg://demoButton"
width="33%"/>
</layout>
</window>
import com.haulmont.cuba.gui.components.Button;
import com.haulmont.cuba.gui.components.HtmlAttributes;
import com.haulmont.cuba.gui.screen.Screen;
import com.haulmont.cuba.gui.screen.Subscribe;
import com.haulmont.cuba.gui.screen.UiController;
import com.haulmont.cuba.gui.screen.UiDescriptor;
import javax.inject.Inject;
@UiController("demo_DemoScreen")
@UiDescriptor("demo-screen.xml")
public class DemoScreen extends Screen {
@Inject
private Button demoButton;
@Inject
protected HtmlAttributes html;
@Subscribe
private void onBeforeShow(BeforeShowEvent event) {
html.setDomAttribute(demoButton, HtmlAttributes.DOM.TITLE, "Hello!");
html.setCssProperty(demoButton, HtmlAttributes.CSS.BACKGROUND_COLOR, "red");
html.setCssProperty(demoButton, HtmlAttributes.CSS.BACKGROUND_IMAGE, "none");
html.setCssProperty(demoButton, HtmlAttributes.CSS.BOX_SHADOW, "none");
html.setCssProperty(demoButton, HtmlAttributes.CSS.BORDER_COLOR, "red");
html.setCssProperty(demoButton, "color", "white");
html.setCssProperty(demoButton, HtmlAttributes.CSS.MAX_WIDTH, "400px");
}
}
3.5.11. 键盘快捷键
本节提供可在应用程序通用用户界面中使用的键盘快捷键列表。下面列出的所有应用程序属性都属于 ClientConfig
接口,可以在 Web 客户端应用程序模块中使用。
-
主应用程序窗口。
-
CTRL-SHIFT-PAGE_DOWN – 切换到下一个标签页。由
cuba.gui.nextTabShortcut
属性定义。 -
CTRL-SHIFT-PAGE_UP – 切换到上一个标签页。由
cuba.gui.previousTabShortcut
属性定义。
-
-
文件夹面板。
-
ENTER – 打开选择的文件夹。
-
SPACE - 选择/取消选择获得焦点的文件夹。
-
ARROW UP 、 ARROW DOWN - 切换文件夹。
-
ARROW LEFT 、 ARROW RIGHT - 折叠/展开包含子文件夹的文件夹或跳转到已有文件夹。
-
-
界面。
-
列表组件(表格、分组表格、树形表格、树)的标准操作。除了这些应用程序属性之外,还可以通过调用其
setShortcut()
方法来设置特定操作的快捷键。-
CTRL-\ – 调用 CreateAction。由
cuba.gui.tableShortcut.insert
属性定义。 -
CTRL-ALT-\ – 调用 AddAction。由
cuba.gui.tableShortcut.add
属性定义。 -
ENTER – 调用 EditAction。由
cuba.gui.tableShortcut.edit
属性定义。 -
CTRL-DELETE – 调用 RemoveAction 和 ExcludeAction。由
cuba.gui.tableShortcut.remove
属性定义。
-
-
下拉列表(LookupField 、LookupPickerField)。
-
SHIFT-DELETE – 清除值。
-
-
Lookup 控件的标准操作(PickerField、LookupPickerField、SearchPickerField)。除了这些应用程序属性外,还可以通过调用其
setShortcut()
方法来设置特定操作的快捷键。-
CTRL-ALT-L – 调用 LookupAction。由
cuba.gui.pickerShortcut.lookup
定义。 -
CTRL-ALT-O – 调用 OpenAction。由
cuba.gui.pickerShortcut.open
属性定义。 -
CTRL-ALT-C – 调用 ClearAction。由
cuba.gui.pickerShortcut.clear
属性定义。
除了这些快捷键外,Lookup 控件还支持使用 CTRL-ALT-1 、CTRL-ALT-2 等操作调用,具体取决于操作个数。如果点击 CTRL-ALT-1,将调用列表中的第一个操作;点击 CTRL-ALT-2 调用第二个操作等。CTRL-ALT 组合可以替换为
cuba.gui.pickerShortcut.modifiers
属性中指定的任何其它组合。 -
-
Filter 组件。
-
SHIFT-BACKSPACE – 打开过滤器选择弹出窗口。由
cuba.gui.filterSelectShortcut
属性定义。 -
SHIFT-ENTER – 应用选择的过滤器。由
cuba.gui.filterApplyShortcut
属性定义。
-
3.5.12. URL 历史及导航
CUBA URL 历史和导航功能可以提供浏览器历史记录和导航功能,这对于很多 web 应用程序来说是最基本的功能。此功能包含以下部分:
-
历史 – 支持浏览器的 后退(Back) 按钮。但是不支持 前进(Forward) 按钮,因为没法重现打开界面的所有条件。
-
路由以及导航 – 注册和处理应用程序界面的路由。
-
路由 API – 一组方法,用来在 URL 中反映界面的当前状态。
fragment 是 URL 里“#”后面的部分,这部分用来做 路由 值。
比如,下面这个 URL:
host:port/app/#main/42/orders/edit?id=17
这个 URL 中,fragment 是 main/42/orders/edit?id=17
,由以下部分组成:
-
main
– 根界面的路由(主窗口); -
42
– 一个 状态标记(state mark),导航机制内部使用; -
orders/edit
– 嵌套的界面路由; -
?id=17
– 参数部分;
所有打开的界面都会将它们的路由映射到当前的 URL。比如,当用户浏览界面打开并且是当前界面的时候,应用程序的 URL 可能是这样:
http://localhost:8080/app/#main/0/users
如果界面没有已经注册的路由,那么只会在 URL fragment 中添加状态标记。示例:
http://localhost:8080/app/#main/42
对于编辑界面,如果界面有注册的路由,则会在地址后面以参数形式加上被编辑实体的 id。示例:
http://localhost:8080/app/#main/1/users/edit?id=27zy3tj6f47p2e3m4w58vdca9y
UUID 类型的标识符会使用 Base32 Crockford 进行加密,其他类型则不会加密。
当没有用户登录的时候,又出于某些原因需要界面路由,则会使用重定向(redirect)参数。假设在地址栏中输入 app/#main/orders
。当应用程序加载并且显示登录界面之后,地址会变成:app/#login?redirectTo=orders
。在成功登录之后,才会打开 orders
路由对应的界面。
如果请求的路由不存在,应用程序会显示一个带有"Not Found"标题的空界面。
URL 历史和导航功能默认开启。设置应用程序属性cuba.web.urlHandlingMode为 NONE
可以关闭此功能,或者设置为 BACK_ONLY
回退到处理浏览器返回按钮的旧机制。
3.5.12.1. 处理 URL 改动
框架能根据应用程序 URL 的变动自动做出响应:会尝试对请求的路由进行解析然后进行历史导航,或者为注册了的路由打开新界面。
当界面通过带参数的路由打开时,框架会在界面显示前先给界面控制器发送 UrlParamsChangedEvent
事件,如果在界面打开了之后 URL 参数发生变化,框架也会做同样的事情。可以订阅此事件来处理界面的初始化参数或者参数的变化。比如,可以根据 URL 参数来加载数据或者隐藏/展示特定的界面 UI 组件。
在界面控制器订阅此事件的示例:
@Subscribe
protected void onUrlParamsChanged(UrlParamsChangedEvent event) {
// handle
}
使用 UrlParamsChangedEvent
的完整示例请参阅后面章节。
3.5.12.2. 路由 API
本章节介绍路由 API 的关键内容。
- 路由注册
-
要为一个界面注册路由,需要在界面控制器添加
@Route
注解,示例:@Route("my-screen") public class MyScreen extends Screen { }
该注解有三个参数:
如果需要为旧界面定义路由,可以在screens.xml文件中为界面元素添加
route
属性(routeParentPrefix
可选,即对应parentPrefix
参数;rootRoute
对应root
参数),示例:<screen id="myScreen" template="..." route="my-screen" />
- 路由压缩
-
此功能目的是为了在打开多个带有相同部分路由的界面时保持 URL 干净易读。举例说明:
对
Order
实体,假设有浏览和编辑界面:@Route("orders") public class OrderBrowser extends StandardLookup<Order> { } @Route("orders/edit") public class OrderEditor extends StandardEditor<Order> { }
在打开浏览界面之后马上打开编辑界面,就能用上 URL 压缩,此时,URL 压缩用来避免 URL 中重复的
orders
路由部分。需要在编辑界面的@Route
注解中用parentPrefix
参数指定路由的重复部分:@Route("orders") public class OrderBrowser extends StandardLookup<Order> { } @Route("orders/edit", parentPrefix = "orders") public class OrderEditor extends StandardEditor<Order> { }
现在,当跟浏览界面在同一标签页打开编辑界面时,地址将像这样:
app/#main/0/orders/edit?id=…
- UI 状态与 URL 的映射
-
使用
UrlRouting
bean 可以根据当前界面和一些参数来更改当前应用程序的 URL。其包含如下方法:-
pushState()
– 更改地址并添加新的浏览器历史记录; -
replaceState()
– 替换地址但不添加新的浏览器历史记录; -
getState()
– 将当前状态作为NavigationState
对象返回。
pushState()/replaceState()
方法接受当前界面控制器和一组参数(map 形式,可选)为输入参数。使用
UrlRouting
的示例,请参阅后面部分。 -
3.5.12.3. 使用 URL 历史和导航 API
本章节包含使用 URL 历史和导航 API 的示例。
假设有 Task - 任务
实体和 TaskInfo
界面,用来展示选中任务的信息。
TaskInfo
界面控制器包含 @Route
注解,用来指定此界面的路由:
package com.company.demo.web.navigation;
import com.haulmont.cuba.gui.Route;
import com.haulmont.cuba.gui.screen.Screen;
import com.haulmont.cuba.gui.screen.UiController;
import com.haulmont.cuba.gui.screen.UiDescriptor;
@Route("task-info")
@UiController("demo_TaskInfoScreen")
@UiDescriptor("task-info.xml")
public class TaskInfoScreen extends Screen {
}
然后,用户可以在浏览器的地址栏输入 http://localhost:8080/app/#main/task-info
来打开此界面:
当界面打开时,地址还包含一个状态标记。
- 状态与 URL 的映射
-
假设
TaskInfo
界面每次展示一个任务的信息,并且能控制切换选取的任务。也许需要在 URL 中反应当前查看的任务,这样可以拷贝 URL,以便之后可以在浏览器的地址栏粘贴 URL 就能打开查看此特定任务的界面。下面的代码实现了选中任务和 URL 的映射:
package com.company.demo.web.navigation; import com.company.demo.entity.Task; import com.google.common.collect.ImmutableMap; import com.haulmont.cuba.gui.Route; import com.haulmont.cuba.gui.UrlRouting; import com.haulmont.cuba.gui.components.Button; import com.haulmont.cuba.gui.components.LookupField; import com.haulmont.cuba.gui.screen.*; import com.haulmont.cuba.web.sys.navigation.UrlIdSerializer; import javax.inject.Inject; @Route("task-info") @UiController("demo_TaskInfoScreen") @UiDescriptor("task-info.xml") @LoadDataBeforeShow public class TaskInfoScreen extends Screen { @Inject private LookupField<Task> taskField; @Inject private UrlRouting urlRouting; @Subscribe("selectBtn") protected void onSelectBtnClick(Button.ClickEvent event) { Task task = taskField.getValue(); (1) if (task == null) { urlRouting.replaceState(this); (2) return; } String serializedTaskId = UrlIdSerializer.serializeId(task.getId()); (3) urlRouting.replaceState(this, ImmutableMap.of("task_id", serializedTaskId)); (4) } }
1 - 从 LookupField
获取当前任务2 - 如果没有选中任务,移除 URL 参数 3 - 使用 UrlIdSerializer
来序列化任务的 id4 - 用包含序列化任务 id 参数的新状态替换当前的 URL 状态。 结果是,当用户选中任务然后点击 Select Task 按钮时,应用程序 URL 会变化:
- UrlParamsChangedEvent
-
现在实现最后一个需求:当用户输入带路由和
task_id
参数的 URL 时,应用程序必须展示相应任务的界面。下面是完整的界面控制器代码。package com.company.demo.web.navigation; import com.company.demo.entity.Task; import com.google.common.collect.ImmutableMap; import com.haulmont.cuba.core.global.DataManager; import com.haulmont.cuba.gui.Route; import com.haulmont.cuba.gui.UrlRouting; import com.haulmont.cuba.gui.components.Button; import com.haulmont.cuba.gui.components.LookupField; import com.haulmont.cuba.gui.navigation.UrlParamsChangedEvent; import com.haulmont.cuba.gui.screen.*; import com.haulmont.cuba.web.sys.navigation.UrlIdSerializer; import javax.inject.Inject; import java.util.UUID; @Route("task-info") @UiController("demo_TaskInfoScreen") @UiDescriptor("task-info.xml") @LoadDataBeforeShow public class TaskInfoScreen extends Screen { @Inject private LookupField<Task> taskField; @Inject private UrlRouting urlRouting; @Inject private DataManager dataManager; @Subscribe protected void onUrlParamsChanged(UrlParamsChangedEvent event) { String serializedTaskId = event.getParams().get("task_id"); (1) UUID taskId = (UUID) UrlIdSerializer.deserializeId(UUID.class, serializedTaskId); (2) taskField.setValue(dataManager.load(Task.class).id(taskId).one()); (3) } @Subscribe("selectBtn") protected void onSelectBtnClick(Button.ClickEvent event) { Task task = taskField.getValue(); if (task == null) { urlRouting.replaceState(this); return; } String serializedTaskId = UrlIdSerializer.serializeId(task.getId()); urlRouting.replaceState(this, ImmutableMap.of("task_id", serializedTaskId)); } }
1 - 从 UrlParamsChangedEvent
获取参数值2 - 对任务 id 进行反序列化 3 - 加载任务实例并设置到界面控件
3.5.13. 插件工厂
插件工厂机制扩展了标准组件的创建过程,允许在 FieldGroup、 Table 和 DataGrid 中创建不同的编辑字段。这意味着应用程序组件或应用程序项目本身可以提供自定义策略,以创建非标准的组件或支持自定义数据类型。
该机制的入口点是 UiComponentsGenerator.generate(ComponentGenerationContext)
方法。其工作原理如下:
-
尝试查找
ComponentGenerationStrategy
实现。如果找到至少一种策略,那么:-
根据
org.springframework.core.Ordered
接口遍历策略。 -
返回第一个创建的非
null
组件。
-
ComponentGenerationStrategy
实现用于创建 UI 组件。项目可以包含任意数量的此类策略。
ComponentGenerationContext
是一个类,该类存储创建组件时可以使用的以下信息:
-
metaClass
- 定义为其创建组件的实体。 -
property
- 定义为其创建组件的实体属性。 -
datasource
- 数据源。 -
optionsDatasource
- 可用于显示选项列表的数据源。 -
valueSource
- 可用于创建组件的值来源。 -
options
- 可用于显示选项的选项对象。 -
xmlDescriptor
- 在组件以声明的方式在 XML 描述中定义时,包含附加信息的 XML 描述。 -
componentClass
- 要创建的组件的类型。例如,FieldGroup
、Table
、DataGrid
。
有两种内置组件策略:
-
DefaultComponentGenerationStrategy
- 用于根据给定的ComponentGenerationContext
对象创建组件。顺序值为ComponentGenerationStrategy.LOWEST_PLATFORM_PRECEDENCE
(1000)。 -
DataGridEditorComponentGenerationStrategy
- 用于根据给定的ComponentGenerationContext
对象为数据网格编辑器创建组件。顺序值为ComponentGenerationStrategy.HIGHEST_PLATFORM_PRECEDENCE + 30
(130)。
以下示例展示如何替换为特定实体的某个属性 FieldGroup
组件的默认生成过程。
import com.company.sales.entity.Order;
import com.haulmont.chile.core.model.MetaClass;
import com.haulmont.cuba.core.global.Metadata;
import com.haulmont.cuba.gui.UiComponents;
import com.haulmont.cuba.gui.components.*;
import com.haulmont.cuba.gui.components.data.ValueSource;
import org.springframework.core.Ordered;
import javax.annotation.Nullable;
import javax.inject.Inject;
import java.sql.Date;
@org.springframework.stereotype.Component(SalesComponentGenerationStrategy.NAME)
public class SalesComponentGenerationStrategy implements ComponentGenerationStrategy, Ordered {
public static final String NAME = "sales_SalesComponentGenerationStrategy";
@Inject
private UiComponents uiComponents;
@Inject
private Metadata metadata;
@Nullable
@Override
public Component createComponent(ComponentGenerationContext context) {
String property = context.getProperty();
MetaClass orderMetaClass = metadata.getClassNN(Order.class);
// Check the specific field of the Order entity
// and that the component is created for the FieldGroup component
if (orderMetaClass.equals(context.getMetaClass())
&& "date".equals(property)
&& context.getComponentClass() != null
&& FieldGroup.class.isAssignableFrom(context.getComponentClass())) {
DatePicker<Date> datePicker = uiComponents.create(DatePicker.TYPE_DATE);
ValueSource valueSource = context.getValueSource();
if (valueSource != null) {
//noinspection unchecked
datePicker.setValueSource(valueSource);
}
return datePicker;
}
return null;
}
@Override
public int getOrder() {
return 50;
}
}
请注意,在更改返回的组件类型时,覆盖现有的生成策略可能会产生错误,因为界面控制器中的代码可能依赖于特定的组件类型。 例如,在使用上述策略的情况下,以下注入将产生异常:
如果尝试打开此类界面,则会出现以下异常: IllegalArgumentException: Can not set com.haulmont.cuba.gui.components.DateField field com.company.sales.web.order.OrderEdit.dateField to com.haulmont.cuba.web.gui.components.WebDatePicker |
以下示例展示如何为特定的 datatype 定义 ComponentGenerationStrategy
。
import com.company.colordatatype.datatypes.ColorDatatype;
import com.haulmont.chile.core.datatypes.Datatype;
import com.haulmont.chile.core.model.MetaClass;
import com.haulmont.chile.core.model.MetaPropertyPath;
import com.haulmont.chile.core.model.Range;
import com.haulmont.cuba.core.app.dynamicattributes.DynamicAttributesUtils;
import com.haulmont.cuba.gui.UiComponents;
import com.haulmont.cuba.gui.components.ColorPicker;
import com.haulmont.cuba.gui.components.Component;
import com.haulmont.cuba.gui.components.ComponentGenerationContext;
import com.haulmont.cuba.gui.components.ComponentGenerationStrategy;
import com.haulmont.cuba.gui.components.data.ValueSource;
import org.springframework.core.annotation.Order;
import javax.annotation.Nullable;
import javax.inject.Inject;
@Order(100)
@org.springframework.stereotype.Component(ColorComponentGenerationStrategy.NAME)
public class ColorComponentGenerationStrategy implements ComponentGenerationStrategy {
public static final String NAME = "colordatatype_ColorComponentGenerationStrategy";
@Inject
private UiComponents uiComponents;
@Nullable
@Override
public Component createComponent(ComponentGenerationContext context) {
String property = context.getProperty();
MetaPropertyPath mpp = resolveMetaPropertyPath(context.getMetaClass(), property);
if (mpp != null) {
Range mppRange = mpp.getRange();
if (mppRange.isDatatype()
&& ((Datatype) mppRange.asDatatype()) instanceof ColorDatatype) {
ColorPicker colorPicker = uiComponents.create(ColorPicker.class);
colorPicker.setDefaultCaptionEnabled(true);
ValueSource valueSource = context.getValueSource();
if (valueSource != null) {
//noinspection unchecked
colorPicker.setValueSource(valueSource);
}
return colorPicker;
}
}
return null;
}
protected MetaPropertyPath resolveMetaPropertyPath(MetaClass metaClass, String property) {
MetaPropertyPath mpp = metaClass.getPropertyPath(property);
if (mpp == null && DynamicAttributesUtils.isDynamicAttribute(property)) {
mpp = DynamicAttributesUtils.getMetaPropertyPath(metaClass, property);
}
return mpp;
}
}
3.5.14. 使用 Vaadin 组件
要在 Web 客户端直接使用实现了可视化组件库中所述组件接口的 Vaadin 组件,请使用以下 Component
接口方法
-
unwrap()
– 获取给定 CUBA 组件的底层 Vaadin 组件。 -
unwrapComposition()
- 获取 Vaadin 组件,该组件是给定 CUBA 组件实现中的最外层封装容器。对于简单的组件,例如Button,此方法返回与unwrap()
-com.vaadin.ui.Button
相同的对象。对于复杂的组件,例如Table,unwrap()
将返回相应的对象-com.vaadin.ui.Table
,而unwrapComposition()
将返回com.vaadin.ui.VerticalLayout
,它包含表格(Table)以及与其一起定义的ButtonsPanel和RowsCount
。
这些方法接收要返回的底层组件的类,例如:
com.vaadin.ui.TextField vTextField = textField.unwrap(com.vaadin.ui.TextField.class);
还可以使用 WebComponentsHelper
类的 unwrap()
和 getComposition()
静态方法,将 CUBA 组件传递给它们。
请注意,如果界面位于项目的 gui 模块中,则只能使用 CUBA 组件的通用接口。要使用组件的 unwrap()
方法,应该将整个界面放入 web 模块,或使用控制器友类机制。
3.5.15. 自定义可视化组件
本节概述了在 CUBA 应用程序中创建自定义 web UI 组件的不同方法。使用这些方法的实用教程位于 创建自定义可视化组件 部分。
可以使用以下技术创建新组件:
-
在 Vaadin 扩展的基础上。
这是最简单的方法。在应用程序中使用扩展需要执行以下步骤:
-
添加扩展工件的坐标到 build.gradle.
-
在项目中创建 web-toolkit 模块。此模块包含一个 GWT widgetset 文件,用于创建可视化组件的客户端部分。
-
将扩展项 widgetset 包含到项目的 widgetset 中。
-
如果组件的外观不适合应用程序主题,请创建主题扩展并为新组件定义一些 CSS。
请参阅 使用第三方 Vaadin 组件 部分中的示例。
-
-
对 JavaScript 库进行包装。
如果已经拥有可以满足需要的 JavaScript 组件,则建议使用此方法。要使用这种方式,需要执行以下操作:
-
在 web 模块中创建服务端 Vaadin 组件。服务端组件为服务代码、访问方法、事件监听等定义 API,服务端组件必须继承
AbstractJavaScriptComponent
类。请注意,继承 JavaScript 组件时,不需要带有 widgetset 的 web-toolkit 模块。 -
创建 JavaScript 连接器。连接器是一个用于初始化 JavaScript 组件并负责 JavaScript 和服务端代码之间的交互的函数。
-
创建一个状态类。在其公共字段中定义从服务端发送到客户端的数据。此类必须继承
JavaScriptComponentState
。
请参阅使用 JavaScript 库章节中的示例。
-
-
发布为新的 GWT 组件。
这是创建全新可视化组件的推荐方法。在应用程序中创建和使用 GWT 组件需要执行以下步骤:
-
创建 web-toolkit 模块。
-
创建一个客户端 GWT 部件类。
-
创建一个服务端 Vaadin 组件。
-
创建一个组件状态类来定义客户端和服务端之间发送的数据。
-
创建一个连接器类来链接客户端代码与服务端组件。
-
创建一个 RPC 接口,用于定义从客户端调用的服务器 API。
请参阅 创建 GWT 组件 章节中的示例。
-
将新组件集成到平台中有三个级别。
-
在第一级,新组件可用作本地 Vaadin 组件。应用程序开发人员可以直接在界面控制器中使用此组件:创建新实例并将其添加到 unwrapped 容器中。上述所有创建新组件的方法都支持这个级别的组件集成。
-
在第二级,将新组件集成到 CUBA 通用 UI 中。在这种情况下,从应用程序开发者的角度来看,它看起来与可视化组件库中的标准组件相同。开发人员可以在界面 XML 描述中定义组件,或者通过控制器中的
UiComponents
创建组件。请参阅 集成 Vaadin 组件到通用 UI 中 章节中的示例。 -
在第三级,新组件可在 Studio 组件面板上使用,并可在 WYSIWYG 布局编辑器中使用。请参阅 CUBA Studio 对自定义可视化组件的支持 章节中的示例。
3.5.15.1. 使用 WebJars
此方法允许使用打包到 JAR 文件中并在 Maven Central 上部署的各种 JS 库。要在应用程序中使用来自 WebJar 的组件需要执行以下步骤:
-
添加依赖到 web 模块的
compile
方法:compile 'org.webjars.bower:jrcarousel:1.0.0'
-
创建 web-toolkit 模块。
-
创建一个客户端 GWT 部件(widget)类并实现用于创建组件的 JSNI 方法。
-
使用
@WebJarResource
注解创建服务端组件类。这个注解只能用于
ClientConnector
继承者(通常是来自 web-toolkit 模块的 UI 组件类)。@WebJarResource
注解值(或资源定义) 应使用下面的格式之一:-
<webjar_name>:<sub_path>
,例如:@WebJarResource("pivottable:plugins/c3/c3.min.css")
-
<webjar_name>/<resource_version>/<webjar_resource>
,例如:@WebJarResource("jquery-ui/1.12.1/jquery-ui.min.js")
注解值可以包含一个或多个 WebJar 资源字符串定义,多个资源使用字符串数组表示:
@WebJarResource({ "jquery-ui:jquery-ui.min.js", "jquery-fileupload:jquery-fileupload.min.js", "jquery-fileupload:jquery-fileupload.min.js" }) public class CubaFileUpload extends CubaAbstractUploadComponent { ... }
WebJar 版本不是必须的,因为 Maven 版本解析策略将自动使用高版本的 WebJar。
或者,可以在
VAADIN/webjars/
中指定一个目录来提供静态资源。这样,可以通过在此目录中放入新版本的资源来覆盖 WebJar 资源。要设置路径,请使用@WebJarResource
注解的overridePath
属性,例如:@WebJarResource(value = "pivottable:plugins/c3/c3.min.css", overridePath = "pivottable")
-
-
将新组件添加到界面。
3.5.15.2. 通用 JavaScriptComponent
JavaScriptComponent
是个简单的 UI 组件,通过它可以使用任何纯 JavaScript 组件,并且这个 JavaSctipt 组件不需要对应的 Vaadin 实现。因此,通过这个组件可以很容易地在基于 CUBA 的项目中集成任何纯 JavaScript 组件。
该组件可以在界面的 XML 描述中以声明的方式定义,因此可以在 XML 中配置动态属性和 JavaScript 依赖。
该组件的 XML 名称: jsComponent
。
- 定义依赖
-
可以为该组件定义一个依赖列表(
js
、css
)。可以使用下面的格式定义依赖:-
WebJar 资源 - 以
webjar://
开头。 -
VAADIN 目录下的文件 - 以
vaadin://
开头。
示例:
jsComponent.addDependencies( "webjar://leaflet.js", "webjar://leaflet-draw.js" );
-
- 定义初始化函数
-
该组件需要一个初始化函数。此函数的名称用来查找 JavaScript 组件连接器(connector)的入口(见下例)。
初始化函数的名称在一个 WEB 浏览器窗口内必须唯一。
函数名称可以通过
setInitFunctionName()
方法传递给组件:jsComponent.setInitFunctionName("com_company_demo_web_screens_Sandbox");
- 定义 JavaScript 连接器(connector)
-
要使用
JavaScriptComponent
来包装 JavaScript 库,需要定义 JavaScript 连接器,其功能主要是初始化 JavaScript 组件并且处理服务端和 JavaScript 代码之间的通信。连接器函数中可以使用下面的方法:
-
this.getElement()
返回组件的 HTML DOM 元素。 -
this.getState()
返回与服务端同步的带有当前状态的共享状态对象。
-
- 组件功能
-
JavaScriptComponent
组件有下列功能:-
设置一个状态对象,该对象可以在客户端层的 JavaScript 连接器中使用,并且可以通过组件状态的
data
字段访问,示例:MyState state = new MyState(); state.minValue = 0; state.maxValue = 100; jsComponent.setState(state);
-
注册一个函数,该函数可以在 JavaScript 中使用提供的名称进行调用,示例:
jsComponent.addFunction("valueChanged", callbackEvent -> { JsonArray arguments = callbackEvent.getArguments(); notifications.create() .withCaption(StringUtils.join(arguments, ", ")) .show(); });
this.valueChanged(values);
-
调用命名的函数,该函数由连接器的 JavaScript 代码添加到包装的对象中。
jsComponent.callFunction("showNotification ");
this.showNotification = function () { alert("TEST"); };
-
- JavaScriptComponent 使用示例
-
本节介绍如何在基于 CUBA 的应用中集成第三方 JavaScript 库,使用 https://quilljs.com/ 的 Quill 富文本编辑器作为例子。请按照下面的步骤集成。
-
在 web 模块添加以下依赖:
compile('org.webjars.npm:quill:1.3.6')
-
在 web 模块的
web/VAADIN/quill
目录内创建quill-connector.js
文件。 -
在此文件内,添加连接器的实现:
com_company_demo_web_screens_Sandbox = function () { var connector = this; var element = connector.getElement(); element.innerHTML = "<div id=\"editor\">" + "<p>Hello World!</p>" + "<p>Some initial <strong>bold</strong> text</p>" + "<p><br></p>" + "</div>"; connector.onStateChange = function () { var state = connector.getState(); var data = state.data; var quill = new Quill('#editor', data.options); // Subscribe on textChange event quill.on('text-change', function (delta, oldDelta, source) { if (source === 'user') { connector.valueChanged(quill.getText(), quill.getContents()); } }); } };
-
创建一个界面,包含以下
jsComponent
定义:<jsComponent id="quill" initFunctionName="com_company_demo_web_screens_Sandbox" height="200px" width="400"> <dependencies> <dependency path="webjar://quill:dist/quill.js"/> <dependency path="webjar://quill:dist/quill.snow.css"/> <dependency path="vaadin://quill/quill-connector.js"/> </dependencies> </jsComponent>
-
添加下面的界面控制器实现:
@UiController("demo_Sandbox") @UiDescriptor("sandbox.xml") public class Sandbox extends Screen { @Inject private JavaScriptComponent quill; @Inject private Notifications notifications; @Subscribe protected void onInit(InitEvent event) { QuillState state = new QuillState(); state.options = ParamsMap.of("theme", "snow", "placeholder", "Compose an epic..."); quill.setState(state); quill.addFunction("valueChanged", javaScriptCallbackEvent -> { String value = javaScriptCallbackEvent.getArguments().getString(0); notifications.create() .withCaption(value) .withPosition(Notifications.Position.BOTTOM_RIGHT) .show(); }); } class QuillState { public Map<String, Object> options; } }
执行结果,界面中可以看到 Quill 富文本编辑器:
另一个集成自定义 JavaScript 组件的例子可以参阅 使用 JavaScript 库。
-
3.5.15.3. 创建自定义可视化组件
如自定义可视化组件部分所述,可以在项目中扩展标准的可视化组件集。有以下几种方式:
-
集成 Vaadin 扩展。许多第三方 Vaadin 组件作为扩展发布,可从
https://vaadin.com/directory
获取。 -
集成 JavaScript 组件。可以使用 JavaScript 库创建 Vaadin 组件。
-
使用 GWT 编写组件的客户端部分来创建新的 Vaadin 组件。
此外,可以将生成的 Vaadin 组件集成到 CUBA 通用 UI 中,以便能够在界面 XML 描述中以声明的方式使用它并绑定到数据源。
集成的最后一步是在 Studio WYSIWYG 布局编辑器中支持新组件。
本节提供了使用上述所有方法创建新可视化组件的示例。集成通用 UI 和提供 Studio 中的支持对于所有方法都是相同的,因此这方面的内容仅在基于 Vaadin 扩展创建新组件的章节进行了描述。
3.5.15.3.1. 使用第三方 Vaadin 组件
这是在应用程序项目中使用 http://vaadin.com/addon/stepper
中提供的 Stepper 组件的示例。该组件允许使用键盘、鼠标滚动或组件右侧的上/下按钮逐步更改文本框的值。
在 CUBA Studio 中创建一个新项目,并将其命名为 addon-demo
。
只能在具有 web-toolkit 模块的应用程序项目中集成 Vaadin 扩展。使用 CUBA Studio 可以很方便的创建这个模块:在主菜单,点击 CUBA > Advanced > Manage modules > Create 'web-toolkit' Module。
然后添加 vaadin 扩展需要的依赖:
-
在build.gradle中,在 web 模块配置中添加包含组件的扩展包的依赖:
configure(webModule) { ... dependencies { ... compile("org.vaadin.addons:stepper:2.4.0") }
-
web-toolkit 模块的
AppWidgetSet.gwt.xml
文件中,说明项目的部件继承自扩展的部件:<module> <inherits name="com.haulmont.cuba.web.widgets.WidgetSet" /> <inherits name="org.vaadin.risto.stepper.StepperWidgetset" /> <set-property name="user.agent" value="safari" /> </module>
可以通过定义
user.agent
属性来加速部件编译。在此示例中,部件仅针对基于 WebKit 的浏览器(Chrome、Safari 等)进行编译。
现在,来自 Vaadin 扩展的组件被包含到项目中。我们看看如何在项目界面中使用它。
-
创建包含下面两个字段的新实体
Customer
:-
String 类型的
name
-
Integer 类型的
score
-
-
为新实体生成标准界面。确保 Module 字段设置为
Module: 'app-web_main'
(这个字段只有在项目添加了 gui 模块之后才会显示)。直接使用 Vaadin 组件的界面必须放在 web 模块中。实际上,界面也可以放在 gui 模块中,但是这就需要使用 Vaadin 组件的代码移动到单独的companion。
-
接下来,我们将
stepper
组件添加到界面上。将
customer-edit.xml
界面的form
组件的score
字段替换成一个hBox
,这个hBox
将用来作为 Vaadin 组件的容器。<?xml version="1.0" encoding="UTF-8" standalone="no"?> <window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd" caption="msg://editorCaption" focusComponent="form" messagesPack="com.company.demo.web.customer"> <data> <instance id="customerDc" class="com.company.demo.entity.Customer" view="_local"> <loader/> </instance> </data> <dialogMode height="600" width="800"/> <layout expand="editActions" spacing="true"> <form id="form" dataContainer="customerDc"> <column width="250px"> <textField id="nameField" property="name"/> <!-- A box that will be used as a container for a Vaadin component --> <hbox id="scoreBox" caption="msg://com.company.demo.entity/Customer.score" height="100%" width="100%"/> </column> </form> <hbox id="editActions" spacing="true"> <button action="windowCommitAndClose"/> <button action="windowClose"/> </hbox> </layout> </window>
将以下代码添加到
CustomerEdit.java
控制器:package com.company.demo.web.customer; import com.company.demo.entity.Customer; import com.haulmont.cuba.gui.components.HBoxLayout; import com.haulmont.cuba.gui.screen.*; import com.vaadin.ui.Layout; import org.vaadin.risto.stepper.IntStepper; import javax.inject.Inject; @UiController("demo_Customer.edit") @UiDescriptor("customer-edit.xml") @EditedEntityContainer("customerDc") @LoadDataBeforeShow public class CustomerEdit extends StandardEditor<Customer> { @Inject private HBoxLayout scoreBox; private IntStepper stepper = new IntStepper(); @Subscribe protected void onInit(InitEvent event) { scoreBox.unwrap(Layout.class) .addComponent(stepper); stepper.setSizeFull(); stepper.addValueChangeListener(valueChangeEvent -> getEditedEntity().setScore(valueChangeEvent.getValue())); } @Subscribe protected void onInitEntity(InitEntityEvent<Customer> event) { event.getEntity().setScore(0); } @Subscribe protected void onBeforeShow(BeforeShowEvent event) { stepper.setValue(getEditedEntity().getScore()); } }
onInit()
方法会初始化一个 stepper 组件的实例,可以用unwrap
方法取到 Vaadin 容器的链接,然后将新组件添加进去。数据绑定是通过编程的方式在
onBeforeShow()
方法中为Customer
实例的stepper
组件设置当前值来实现的。此外,对应的实体属性是在用户改变值时,通过值变化的监听器来更新的。 -
要调整组件样式,请在项目中创建主题扩展。使用 CUBA Studio 可以很方便扩展主题,点击 CUBA > Advanced > Manage themes > Create theme extension。在弹出窗口选择
hover
主题。另一个方式时使用 CUBA CLI 的extend-theme
命令。之后,打开位于 web 模块中的themes/hover/com.company.demo/hover-ext.scss
文件并添加以下代码:/* Define your theme modifications inside next mixin */ @mixin com_company_demo-hover-ext { /* Basic styles for stepper inner text box */ .stepper input[type="text"] { @include box-defaults; @include valo-textfield-style; &:focus { @include valo-textfield-focus-style; } } }
-
启动应用程序服务。将生成如下所示的编辑界面:
3.5.15.3.2. 集成 Vaadin 组件到通用 UI 中
在前一节中,我们在项目中包含了第三方 Stepper 组件。在本节中,我们将它集成到 CUBA 通用 UI 中。这样就允许开发人员在界面 XML 描述中以声明方式使用组件,并通过数据组件将其绑定到数据模型实体。
为了在 CUBA 通用 UI 中集成 Stepper,需要创建以下文件:
-
Stepper
- 在 web 模块 gui 子文件夹的该组件接口。 -
WebStepper
- 在 web 模块 gui 子文件夹的该组件实现。 -
StepperLoader
- 在 web 模块 gui 子文件夹的组件 XML 加载器。 -
ui-component.xsd
- 一个新的组件 XML 结构定义。如果这个文件已经存在,在文件中添加关于此新组件的信息。 -
cuba-ui-component.xml
- 在 web 模块中注册新组件加载器的文件。如果该文件已存在,在文件中添加关于此新组件的信息。
在 IDE 中打开项目。
创建相应的文件,并添加必要的更改。
-
在 web 模块的 gui 子文件夹创建
Stepper
接口。用以下代码替换其内容:package com.company.demo.web.gui.components; import com.haulmont.cuba.gui.components.Field; // note that Stepper should extend Field public interface Stepper extends Field<Integer> { String NAME = "stepper"; boolean isManualInputAllowed(); void setManualInputAllowed(boolean value); boolean isMouseWheelEnabled(); void setMouseWheelEnabled(boolean value); int getStepAmount(); void setStepAmount(int amount); int getMaxValue(); void setMaxValue(int maxValue); int getMinValue(); void setMinValue(int minValue); }
组件的基础接口是
Field
,用于显示和编辑实体属性。 -
创建
WebStepper
类 - web 模块的 gui 子文件夹中的组件实现。用以下代码替换其内容:package com.company.demo.web.gui.components; import com.haulmont.cuba.web.gui.components.WebV8AbstractField; import org.vaadin.risto.stepper.IntStepper; // note that WebStepper should extend WebV8AbstractField public class WebStepper extends WebV8AbstractField<IntStepper, Integer, Integer> implements Stepper { public WebStepper() { this.component = createComponent(); attachValueChangeListener(component); } private IntStepper createComponent() { return new IntStepper(); } @Override public boolean isManualInputAllowed() { return component.isManualInputAllowed(); } @Override public void setManualInputAllowed(boolean value) { component.setManualInputAllowed(value); } @Override public boolean isMouseWheelEnabled() { return component.isMouseWheelEnabled(); } @Override public void setMouseWheelEnabled(boolean value) { component.setMouseWheelEnabled(value); } @Override public int getStepAmount() { return component.getStepAmount(); } @Override public void setStepAmount(int amount) { component.setStepAmount(amount); } @Override public int getMaxValue() { return component.getMaxValue(); } @Override public void setMaxValue(int maxValue) { component.setMaxValue(maxValue); } @Override public int getMinValue() { return component.getMinValue(); } @Override public void setMinValue(int minValue) { component.setMinValue(minValue); } }
所选择的基类是
WebV8AbstractField
,其实现了Field
接口的方法。 -
web 模块的 gui 子文件夹中的
StepperLoader
类从 XML 描述中加载组件。package com.company.demo.web.gui.xml.layout.loaders; import com.company.demo.web.gui.components.Stepper; import com.haulmont.cuba.gui.xml.layout.loaders.AbstractFieldLoader; public class StepperLoader extends AbstractFieldLoader<Stepper> { @Override public void createComponent() { resultComponent = factory.create(Stepper.class); loadId(resultComponent, element); } @Override public void loadComponent() { super.loadComponent(); String manualInput = element.attributeValue("manualInput"); if (manualInput != null) { resultComponent.setManualInputAllowed(Boolean.parseBoolean(manualInput)); } String mouseWheel = element.attributeValue("mouseWheel"); if (mouseWheel != null) { resultComponent.setMouseWheelEnabled(Boolean.parseBoolean(mouseWheel)); } String stepAmount = element.attributeValue("stepAmount"); if (stepAmount != null) { resultComponent.setStepAmount(Integer.parseInt(stepAmount)); } String maxValue = element.attributeValue("maxValue"); if (maxValue != null) { resultComponent.setMaxValue(Integer.parseInt(maxValue)); } String minValue = element.attributeValue("minValue"); if (minValue != null) { resultComponent.setMinValue(Integer.parseInt(minValue)); } } }
AbstractFieldLoader
类包含用于加载Field
组件的基本属性的代码。所以StepperLoader
只加载特定于Stepper
组件的属性。 -
web 模块中的
cuba-ui-component.xml
文件注册新组件及其加载器。用以下代码替换其内容:<?xml version="1.0" encoding="UTF-8" standalone="no"?> <components xmlns="http://schemas.haulmont.com/cuba/components.xsd"> <component> <name>stepper</name> <componentLoader>com.company.demo.web.gui.xml.layout.loaders.StepperLoader</componentLoader> <class>com.company.demo.web.gui.components.WebStepper</class> </component> </components>
-
web 模块中的
ui-component.xsd
文件包含自定义可视化组件的 XSD。添加stepper
元素及其属性定义。<?xml version="1.0" encoding="UTF-8" standalone="no"?> <xs:schema xmlns="http://schemas.company.com/agd/0.1/ui-component.xsd" elementFormDefault="qualified" targetNamespace="http://schemas.company.com/agd/0.1/ui-component.xsd" xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:element name="stepper"> <xs:complexType> <xs:attribute name="id" type="xs:string"/> <xs:attribute name="caption" type="xs:string"/> <xs:attribute name="height" type="xs:string"/> <xs:attribute name="width" type="xs:string"/> <xs:attribute name="dataContainer" type="xs:string"/> <xs:attribute name="property" type="xs:string"/> <xs:attribute name="manualInput" type="xs:boolean"/> <xs:attribute name="mouseWheel" type="xs:boolean"/> <xs:attribute name="stepAmount" type="xs:int"/> <xs:attribute name="maxValue" type="xs:int"/> <xs:attribute name="minValue" type="xs:int"/> </xs:complexType> </xs:element> </xs:schema>
我们来看一下如何将新组件添加到界面。
-
可以删除前一章节的改动或者为实体生成编辑界面。
-
将
stepper
组件添加到编辑界面。可以使用声明式的方式或者编程的方式进行添加。我们分别看看这两种方法。-
在 XML 描述中声明式的使用该组件。
-
打开
customer-edit.xml
文件。 -
定义新的命名空间
xmlns:app="http://schemas.company.com/agd/0.1/ui-component.xsd"
。 -
从
form
中删除score
字段。 -
将
stepper
组件添加到界面上。
这时,界面 XML 描述应如下所示:
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd" xmlns:app="http://schemas.company.com/agd/0.1/ui-component.xsd" caption="msg://editorCaption" focusComponent="form" messagesPack="com.company.demo.web.customer"> <data> <instance id="customerDc" class="com.company.demo.entity.Customer" view="_local"> <loader/> </instance> </data> <dialogMode height="600" width="800"/> <layout expand="editActions" spacing="true"> <form id="form" dataContainer="customerDc"> <column width="250px"> <textField id="nameField" property="name"/> <app:stepper id="stepper" dataContainer="customerDc" property="score" minValue="0" maxValue="20"/> </column> </form> <hbox id="editActions" spacing="true"> <button action="windowCommitAndClose"/> <button action="windowClose"/> </hbox> </layout> </window>
在上面的例子中,
stepper
组件与Customer
实体的score
属性相关联。该实体的实例由customerDc
实例容器管理。 -
-
在 Java 控制器中以编程的方式创建组件。
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd" caption="msg://editorCaption" focusComponent="form" messagesPack="com.company.demo.web.customer"> <data> <instance id="customerDc" class="com.company.demo.entity.Customer" view="_local"> <loader/> </instance> </data> <dialogMode height="600" width="800"/> <layout expand="editActions" spacing="true"> <form id="form" dataContainer="customerDc"> <column width="250px"> <textField id="nameField" property="name"/> </column> </form> <hbox id="editActions" spacing="true"> <button action="windowCommitAndClose"/> <button action="windowClose"/> </hbox> </layout> </window>
package com.company.demo.web.customer; import com.company.demo.entity.Customer; import com.company.demo.web.gui.components.Stepper; import com.haulmont.cuba.gui.UiComponents; import com.haulmont.cuba.gui.components.Form; import com.haulmont.cuba.gui.components.data.value.ContainerValueSource; import com.haulmont.cuba.gui.model.InstanceContainer; import com.haulmont.cuba.gui.screen.*; import javax.inject.Inject; @UiController("demo_Customer.edit") @UiDescriptor("customer-edit.xml") @EditedEntityContainer("customerDc") @LoadDataBeforeShow public class CustomerEdit extends StandardEditor<Customer> { @Inject private Form form; @Inject private InstanceContainer<Customer> customerDc; @Inject private UiComponents uiComponents; @Subscribe protected void onInit(InitEvent event) { Stepper stepper = uiComponents.create(Stepper.NAME); stepper.setValueSource(new ContainerValueSource<>(customerDc, "score")); stepper.setCaption("Score"); stepper.setWidthFull(); stepper.setMinValue(0); stepper.setMaxValue(20); form.add(stepper); } @Subscribe protected void onInitEntity(InitEntityEvent<Customer> event) { event.getEntity().setScore(0); } }
-
-
启动应用程序服务。将生成如下所示的编辑界面:
3.5.15.3.3. 使用 JavaScript 库
在此示例中,我们将使用 jQuery UI 库中的 Slider 组件。拥有两个拖拽手柄的滑动条,用于定义取值范围。
在 CUBA Studio 中创建一个新项目,并将其命名为 jscomponent
。
为了使用 Slider 组件,需要创建以下文件:
-
SliderServerComponent
- 与 JavaScript 集成的 Vaadin 组件。 -
SliderState
- Vaadin 组件的状态类。 -
slider-connector.js
- Vaadin 组件的 JavaScript 连接器。
集成至通用 UI 的过程跟集成 Vaadin 组件到通用 UI 中描述的一致,这里就不再重复了。
下面在 web 模块的 toolkit/ui/slider
子目录创建需要的文件,并做相应修改。
-
SlideState
状态类定义在服务器和客户端之间传输的数据。在这个例子中,它是最小值、最大值和选定值。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 服务端组件
SliderServerComponent
。package com.company.jscomponent.web.toolkit.ui.slider; import com.haulmont.cuba.web.widgets.WebJarResource; import com.vaadin.annotations.JavaScript; import com.vaadin.ui.AbstractJavaScriptComponent; import elemental.json.JsonArray; @WebJarResource({"jquery:jquery.min.js", "jquery-ui:jquery-ui.min.js", "jquery-ui:jquery-ui.css"}) @JavaScript({"slider-connector.js"}) public class SliderServerComponent extends AbstractJavaScriptComponent { public interface ValueChangeListener { void valueChanged(double[] newValue); } private ValueChangeListener listener; public SliderServerComponent() { addFunction("valueChanged", arguments -> { JsonArray array = arguments.getArray(0); double[] values = new double[2]; values[0] = array.getNumber(0); values[1] = array.getNumber(1); getState(false).values = values; listener.valueChanged(values); }); } public void setValue(double[] value) { getState().values = value; } public double[] getValue() { return getState().values; } public double getMinValue() { return getState().minValue; } public void setMinValue(double minValue) { getState().minValue = minValue; } public double getMaxValue() { return getState().maxValue; } public void setMaxValue(double maxValue) { getState().maxValue = maxValue; } @Override protected SliderState getState() { return (SliderState) super.getState(); } @Override public SliderState getState(boolean markAsDirty) { return (SliderState) super.getState(markAsDirty); } public ValueChangeListener getListener() { return listener; } public void setListener(ValueChangeListener listener) { this.listener = listener; } }
服务端组件定义了 getter 方法和 setter 方法来处理滑块状态,也定义了一个值更改监听器接口。该类继承自
AbstractJavaScriptComponent
。在类构造器中的调用
addFunction()
方法为客户端的valueChanged()
方法的 RPC 调用定义了一个处理程序。@JavaScript
和@StyleSheet
注解指向的文件,必须在网页上加载。在这个的示例中,这些是 jquery-ui 库的 JavaScript 文件、位于WebJar 资源的 jquery-ui 的样式表以及 Vaadin 服务组件 Java 包里面的连接器。 -
JavaScript 连接器
slider-connector.js
.com_company_jscomponent_web_toolkit_ui_slider_SliderServerComponent = function() { var connector = this; var element = connector.getElement(); $(element).html("<div/>"); $(element).css("padding", "5px 0px"); var slider = $("div", element).slider({ range: true, slide: function(event, ui) { connector.valueChanged(ui.values); } }); connector.onStateChange = function() { var state = connector.getState(); slider.slider("values", state.values); slider.slider("option", "min", state.minValue); slider.slider("option", "max", state.maxValue); $(element).width(state.width); } }
连接器是一个在加载网页时初始化 JavaScript 组件的函数。该函数名称必须与服务端组件类名对应,其中包名中的点用下划线代替。
Vaadin 为连接器函数添加了几种有用的方法。
this.getElement()
返回组件的 HTML DOM 元素,this.getState()
返回一个状态对象。这里的连接器执行以下操作:
-
初始化 jQuery UI 库的
slider
组件。当滑块的位置发生任何变化时,将调用slide()
函数,该函数又调用连接器的valueChanged()
方法。valuedChanged()
是在服务端SliderServerComponent
类中定义的方法。 -
定义
onStateChange()
函数。在服务端更改状态对象时调用它。
-
为了演示组件的工作原理,我们创建有三个属性的 Product
实体:
-
String 类型的
name
-
Double 类型的
minDiscount
-
Double 类型的
maxDiscount
为实体生成标准界面。确保 Module 字段的值为 Module: 'app-web_main'
(只有在项目添加了 gui 模块之后才会显示这个字段)。
slider
组件将设置产品的最小和最大折扣值。
打开 product-edit.xml
文件。通过将 editable="false"
属性添加到相应的元素,使 minDiscount
和 maxDiscount
字段不可编辑。然后添加一个 box,将作为 Vaadin 组件的容器使用。
这时,编辑界面的 XML 描述应如下所示:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd"
caption="msg://editorCaption"
focusComponent="form"
messagesPack="com.company.jscomponent.web.product">
<data>
<instance id="productDc"
class="com.company.jscomponent.entity.Product"
view="_local">
<loader/>
</instance>
</data>
<dialogMode height="600"
width="800"/>
<layout expand="editActions" spacing="true">
<form id="form" dataContainer="productDc">
<column width="250px">
<textField id="nameField" property="name"/>
<textField id="minDiscountField" property="minDiscount" editable="false"/>
<textField id="maxDiscountField" property="maxDiscount" editable="false"/>
<hbox id="sliderBox" width="100%"/>
</column>
</form>
<hbox id="editActions" spacing="true">
<button action="windowCommitAndClose"/>
<button action="windowClose"/>
</hbox>
</layout>
</window>
打开 ProductEit.java
文件。用以下代码替换其内容:
package com.company.jscomponent.web.product;
import com.company.jscomponent.entity.Product;
import com.company.jscomponent.web.toolkit.ui.slider.SliderServerComponent;
import com.haulmont.cuba.gui.components.HBoxLayout;
import com.haulmont.cuba.gui.screen.*;
import com.vaadin.ui.Layout;
import javax.inject.Inject;
@UiController("jscomponent_Product.edit")
@UiDescriptor("product-edit.xml")
@EditedEntityContainer("productDc")
@LoadDataBeforeShow
public class ProductEdit extends StandardEditor<Product> {
@Inject
private HBoxLayout sliderBox;
@Subscribe
protected void onInitEntity(InitEntityEvent<Product> event) {
event.getEntity().setMinDiscount(15.0);
event.getEntity().setMaxDiscount(70.0);
}
@Subscribe
protected void onBeforeShow(BeforeShowEvent event) {
SliderServerComponent slider = new SliderServerComponent();
slider.setValue(new double[]{
getEditedEntity().getMinDiscount(),
getEditedEntity().getMaxDiscount()
});
slider.setMinValue(0);
slider.setMaxValue(100);
slider.setWidth("250px");
slider.setListener(newValue -> {
getEditedEntity().setMinDiscount(newValue[0]);
getEditedEntity().setMaxDiscount(newValue[1]);
});
sliderBox.unwrap(Layout.class).addComponent(slider);
}
}
onInitEntity()
方法为新产品的折扣设置初始值。
onBeforeShow()
方法初始化 slider
组件,设置 slider
的当前值、最小值和最大值,并定义值更改监听器。当滑块移动时,可编辑实体的相应字段将被设置成新值。
启动应用程序服务并打开产品编辑界面。更改滑块位置时会改变文本框的值。
3.5.15.3.4. 创建 GWT 组件
在本节中,我们介绍如何创建一个简单的 GWT 组件(由 5 颗星组成的评级字段)及其在应用程序界面中的用法。
在 CUBA Studio 中创建一个新项目,并将其命名为 ratingsample
。
创建 web-toolkit 模块。一个简便的方法就是使用 CUBA Studio:在主菜单,点击 CUBA > Advanced > Manage modules > Create 'web-toolkit' Module。
为了创建 GWT 组件,需要创建下列文件:
-
RatingFieldWidget.java
- web-toolkit 模块中的 GWT 部件。 -
RatingFieldServerComponent.java
- Vaadin 组件类。 -
RatingFieldState.java
- 组件状态类。 -
RatingFieldConnector.java
- 连接器,用于连接客户端代码和服务器组件。 -
RatingFieldServerRpc.java
- 为客户端定义服务器 API 的类。
现在创建需要的文件并对其进行必要的更改。
-
在 web-toolkit 模块中创建
RatingFieldWidget
类。使用下面代码作为其内容:package com.company.ratingsample.web.toolkit.ui.client.ratingfield; import com.google.gwt.dom.client.DivElement; import com.google.gwt.dom.client.SpanElement; import com.google.gwt.dom.client.Style.Display; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.ui.FocusWidget; import java.util.ArrayList; import java.util.List; public class RatingFieldWidget extends FocusWidget { private static final String CLASSNAME = "ratingfield"; // API for handle clicks public interface StarClickListener { void starClicked(int value); } protected List<SpanElement> stars = new ArrayList<>(5); protected StarClickListener listener; protected int value = 0; public RatingFieldWidget() { DivElement container = DOM.createDiv().cast(); container.getStyle().setDisplay(Display.INLINE_BLOCK); for (int i = 0; i < 5; i++) { SpanElement star = DOM.createSpan().cast(); // add star element to the container DOM.insertChild(container, star, i); // subscribe on ONCLICK event DOM.sinkEvents(star, Event.ONCLICK); stars.add(star); } setElement(container); setStylePrimaryName(CLASSNAME); } // main method for handling events in GWT widgets @Override public void onBrowserEvent(Event event) { super.onBrowserEvent(event); switch (event.getTypeInt()) { // react on ONCLICK event case Event.ONCLICK: SpanElement element = event.getEventTarget().cast(); // if click was on the star int index = stars.indexOf(element); if (index >= 0) { int value = index + 1; // set internal value setValue(value); // notify listeners if (listener != null) { listener.starClicked(value); } } break; } } @Override public void setStylePrimaryName(String style) { super.setStylePrimaryName(style); for (SpanElement star : stars) { star.setClassName(style + "-star"); } updateStarsStyle(this.value); } // let application code change the state public void setValue(int value) { this.value = value; updateStarsStyle(value); } // refresh visual representation private void updateStarsStyle(int value) { for (SpanElement star : stars) { star.removeClassName(getStylePrimaryName() + "-star-selected"); } for (int i = 0; i < value; i++) { stars.get(i).addClassName(getStylePrimaryName() + "-star-selected"); } } }
部件(Widget)是一个客户端类,负责在 Web 浏览器中显示组件并处理浏览器事件。它定义了与服务端配合起来工作的接口。在这个的例子中,这些接口是
setValue()
方法和StarClickListener
接口。 -
RatingFieldServerComponent
是一个 Vaadin 组件类。它定义了服务端代码 API、访问器方法、事件监听器和数据源连接。开发人员在应用程序代码中使用的是这个类的方法。package com.company.ratingsample.web.toolkit.ui; import com.company.ratingsample.web.toolkit.ui.client.ratingfield.RatingFieldServerRpc; import com.company.ratingsample.web.toolkit.ui.client.ratingfield.RatingFieldState; import com.vaadin.ui.AbstractField; // the field will have a value with integer type public class RatingFieldServerComponent extends AbstractField<Integer> { public RatingFieldServerComponent() { // register an interface implementation that will be invoked on a request from the client registerRpc((RatingFieldServerRpc) value -> setValue(value, true)); } @Override protected void doSetValue(Integer value) { if (value == null) { value = 0; } getState().value = value; } @Override public Integer getValue() { return getState().value; } // define own state class @Override protected RatingFieldState getState() { return (RatingFieldState) super.getState(); } @Override protected RatingFieldState getState(boolean markAsDirty) { return (RatingFieldState) super.getState(markAsDirty); } }
-
RatingFieldState
状态类定义客户端和服务器之间发送的数据。它包含在服务端自动序列化并在客户端上反序列化的公共字段。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; }
-
RatingFieldServerRpc
接口定义了客户端可调用的服务器 API。它的方法可以由 Vaadin 内置的 RPC 机制调用。我们将在此组件中实现此接口。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); }
-
在 web-toolkit 模块中创建
RatingFieldConnector
类,连接器将客户端代码与服务端连接起来。package com.company.ratingsample.web.toolkit.ui.client.ratingfield; import com.company.ratingsample.web.toolkit.ui.RatingFieldServerComponent; import com.vaadin.client.communication.StateChangeEvent; import com.vaadin.client.ui.AbstractFieldConnector; import com.vaadin.shared.ui.Connect; // link the connector with the server implementation of RatingField // extend AbstractField connector @Connect(RatingFieldServerComponent.class) public class RatingFieldConnector extends AbstractFieldConnector { // we will use a RatingFieldWidget widget @Override public RatingFieldWidget getWidget() { RatingFieldWidget widget = (RatingFieldWidget) super.getWidget(); if (widget.listener == null) { widget.listener = value -> getRpcProxy(RatingFieldServerRpc.class).starClicked(value); } return widget; } // our state class is RatingFieldState @Override public RatingFieldState getState() { return (RatingFieldState) super.getState(); } // react on server state change @Override public void onStateChanged(StateChangeEvent stateChangeEvent) { super.onStateChanged(stateChangeEvent); // refresh the widget if the value on server has changed if (stateChangeEvent.hasPropertyChanged("value")) { getWidget().setValue(getState().value); } } }
RatingFieldWidget
类中不定义组件的外观样式,只为关键元素指定样式名称。要定义组件的外观,需要创建样式表文件。简便方法就是使用 CUBA Studio:在主菜单,点击 CUBA > Advanced > Manage themes > Create theme extension。在弹窗中选择 hover
主题。另一个方法是使用 CUBA CLI 的 extend-theme
命令。hover
主题使用了 FontAwesome 的象形符号字体替代了 icons。
建议以 SCSS 混入(Mixin)的形式将组件样式放到 components/componentname
目录中的单独文件 componentname.scss
中。在 web 模块的 themes/hover/com.company.ratingsample
目录中创建 components/ratingfield
目录结构。然后在 ratingfield
目录中创建 ratingfield.scss
文件:
@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;
}
}
将此文件包含在 hover-ext.scss
主题文件中:
@import "components/ratingfield/ratingfield";
@mixin com_company_ratingsample-hover-ext {
@include ratingfield;
}
为了演示组件的工作原理,我们在 web 模块中创建一个新的界面。
将界面命名为 rating-screen
。
在 IDE 中打开 rating-screen.xml
文件。Rating 组件需要一个容器,我们在界面 XML 中声明它:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd"
caption="msg://caption"
messagesPack="com.company.ratingsample.web.screens.rating">
<layout expand="container">
<vbox id="container">
<!-- we'll add vaadin component here-->
</vbox>
</layout>
</window>
打开 RatingScreen.java
界面控制器并添加将组件放置到界面上的代码。
package com.company.ratingsample.web.screens.rating;
import com.company.ratingsample.web.toolkit.ui.RatingFieldServerComponent;
import com.haulmont.cuba.gui.components.VBoxLayout;
import com.haulmont.cuba.gui.screen.Screen;
import com.haulmont.cuba.gui.screen.Subscribe;
import com.haulmont.cuba.gui.screen.UiController;
import com.haulmont.cuba.gui.screen.UiDescriptor;
import com.vaadin.ui.Layout;
import javax.inject.Inject;
@UiController("ratingsample_RatingScreen")
@UiDescriptor("rating-screen.xml")
public class RatingScreen extends Screen {
@Inject
private VBoxLayout container;
@Subscribe
protected void onInit(InitEvent event) {
RatingFieldServerComponent field = new RatingFieldServerComponent();
field.setCaption("Rate this!");
container.unwrap(Layout.class).addComponent(field);
}
}
启动应用程序服务并查看结果。
3.5.16. Web 客户端基础设施
Web 客户端block的通用用户界面基于 Vaadin 框架实现。Web 客户端基础设施中可用的主要类如下所述。
-
App
- 应用程序基础设施的中心类。包含指向Connection
和其它基础设施对象的引用。对于给定的 HTTP 会话,只存在一个App
实例。可以使用App.getInstance()
静态方法获取App
实例的引用。如果要在项目中自定义App
的功能,请在 web 模块的根包中创建一个类继承DefaultApp
,并将其作为cuba_App
bean 注册在 web-spring.xml中,例如:<bean name="cuba_App" class="com.company.sample.web.MyApp" scope="vaadin"/>
-
Connection
是一个接口,此接口提供连接到中间件和保持用户会话的功能。ConnectionImpl
是此接口的标准实现。 -
ExceptionHandlers
类包含客户端级(client-level)异常处理器的集合。 -
AppUI
是一个继承com.vaadin.ui.UI
类的平台类。对于 Web 浏览器打开的每个标签页,都有一个此类的实例。它包含指向TopLevelWindow
实现( 登录窗口或主窗口) 的链接,具体取决于连接状态。可以使用AppUI.getCurrent()
静态方法获取对当前浏览器标签页的AppUI
的引用。 -
AppLoginWindow
– 在用户登录之前显示的窗口。可以通过继承AppLoginWindow
来自定义登录窗口,或者通过继承AbstractWindow创建全新的登录窗口。Studio 中,在 CUBA 项目树中选择 Generic UI > Screens 部分然后点击 New > Screen。然后在 Legacy Screen Templates 标签页选择 Login window 模板。如果在自定义类中重写了init()
方法,确保调用了super.init(params)
方法。 -
AppMainWindow
– 用户登录后显示的主应用程序窗口。可以通过继承AppMainWindow
来自定义主窗口,或者通过继承AbstractMainWindow
并在 XML 描述中定义所需的布局来创建全新的主窗口。Studio 中,在 CUBA 项目树中选择 Generic UI > Screens 部分然后点击 New > Screen。然后在 Legacy Screen Templates 标签页选择 Main window 模板。如果在自定义类中重写了init()
方法,确保调用了super.init(params)
方法。可以使用以下应用程序属性控制某些主窗口参数,而无需重新定义默认实现:
-
cuba.web.appWindowMode – 设置主窗口的默认模式:多标签或单界面(
TABBED
或SINGLE
)。用户后续可以使用 Help > Settings 界面更改模式。 -
cuba.web.maxTabCount – 当主窗口处于标签页模式时,此属性设置用户可以打开的最大标签页数量。默认值为 7。
如果要处理浏览器的 Back 按钮,请在 TopLevelWindow
(登录窗口或主窗口)中实现 CubaHistoryControl.HistoryBackHandler
接口。如果cuba.web.allowHandleBrowserHistoryBack应用程序属性设置为 true,则会调用这个接口实现的 onHistoryBackPerformed()
方法而不是浏览器的标准行为。
3.5.17. 主窗口布局
主窗口由具有 mainWindow
标识符的特定界面定义。它的控制器应该派生自 AbstractMainWindow
类。
除了标准 UI 组件之外,主窗口中还可以使用以下特殊组件:
-
AppMenu
– 应用程序主菜单。 -
FoldersPane
– 应用程序和搜索文件夹面板。 -
AppWorkArea
– 工作区,以THIS_TAB
、NEW_TAB
和NEW_WINDOW
模式打开界面时需要的组件。 -
UserIndicator
– 显示当前用户名称的字段,也可以用于选择替换用户(如果有)。setUserNameFormatter()
方法允许以不同于User
实例名称的格式显示用户名:userIndicator.setUserNameFormatter(value -> value.getName() + " - [" + value.getEmail() + "]");
-
NewWindowButton
– 一个按钮,用于在单独的浏览标签页中打开新的应用程序主窗口。 -
LogoutButton
– 应用程序注销按钮。 -
TimeZoneIndicator
– 显示当前用户时区的标签。 -
FtsField
– 全文搜索框。
要定义特定组件,请在界面上添加 xmlns:main
命名空间:
<window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
xmlns:main="http://schemas.haulmont.com/cuba/mainwindow.xsd"
class="com.company.sample.gui.MainWindow">
<layout>
</layout>
</window>
AppWorkArea
组件用于显示应用程序界面。如果cuba.web.appWindowMode应用程序属性为 TABBED
(默认值),则工作区显示带有打开界面的 TabSheet。否则只打开单个应用程序界面。cuba.web.mainTabSheetMode和cuba.web.managedMainTabSheetMode应用程序属性定义切换标签页时如何处理标签页内容。当没有界面被打开时,工作区显示 initialLayout
元素中定义的组件:
<main:workArea id="workArea" width="100%" height="100%">
<main:initialLayout spacing="true" margin="true">
<!-- content shown when there are no open screens -->
</main:initialLayout>
</main:workArea>
在打开第一个应用程序界面时从 AppWorkArea
中删除初始界面(initialLayout
),在所有界面关闭后初始界面又被重新添加上。可以添加 AppWorkArea.StateChangeListener
来处理工作区中初始界面和应用程序界面之间切换。例如,这里使用这个监听器刷新初始界面的数据。
平台提供两个标准主窗口实现。顶部有横向菜单的默认主窗口,其 XML 描述在 /com/haulmont/cuba/web/app/mainwindow/mainwindow.xml
中,对应的控制器类是 AppMainWindow
。另一个可用的模板包含垂直侧边菜单。
标准实现可以在项目中被继承,和任何其它应用程序界面一样。继承界面的示例:
<window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
xmlns:ext="http://schemas.haulmont.com/cuba/window-ext.xsd"
extends="com/haulmont/cuba/web/app/mainwindow/mainwindow.xml"
class="com.haulmont.cuba.web.app.mainwindow.AppMainWindow">
<layout>
<vbox ext:index="0">
<label value="This is my main window!" stylename="h2"/>
</vbox>
</layout>
</window>
此界面应在web-screens.xml文件中注册,并使用 mainWindow
标识符。
扩展主界面的最简单方法是在 CUBA Studio 中完成:选中 CUBA 项目视图的 Generic UI 部分,在右键菜单中点击 New > Screen ,然后在弹出窗口的 Legacy Screen Templates 标签页为新的主界面选择模板。会在 Web 模块创建新的 ext-mainwindow.xml
文件,并自动注册到 web-screens.xml
中。
CUBA 平台提供两个标准主窗口布局模板:横向菜单和侧边菜单。标准主窗口实现可以被自定义窗口实现完全替换。例如:
<window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
xmlns:main="http://schemas.haulmont.com/cuba/mainwindow.xsd"
class="com.company.sample.gui.MainWindow">
<layout expand="middlePanel">
<hbox margin="true"
stylename="gray"
width="100%">
<label align="MIDDLE_CENTER"
value="Header"/>
</hbox>
<main:menu width="100%"/>
<split id="middlePanel"
orientation="horizontal"
pos="80"
width="100%">
<main:workArea id="workArea"
height="100%"
width="100%">
<main:initialLayout stylename="red">
<label align="MIDDLE_CENTER"
value="Work Area (Initial Layout)"/>
</main:initialLayout>
</main:workArea>
<main:foldersPane height="100%"
stylename="blue"
width="100%"/>
</split>
<hbox margin="true"
stylename="gray"
width="100%">
<label align="MIDDLE_CENTER"
value="Footer"/>
</hbox>
</layout>
</window>
生成的主窗口如下所示:
在这个完全自定义的主窗口中打开应用程序界面:
cuba.web.showBreadCrumbs 应用程序属性允许隐藏打开的界面上方的导航面板。
3.5.18. Web 登录
本节介绍 Web 客户端身份验证的工作原理以及如何在项目中进行扩展。有关中间层身份验证的信息,请参阅登录。
Web 客户端 block 的登录过程的实现机制如下:
-
ConnectionImpl
实现了Connection
。 -
LoginProvider
实现。 -
HttpRequestFilter
实现。
Web 登录子系统的主要接口是 Connection
,它包含以下关键方法:
-
login() - 验证用户、启动会话并更改连接状态。
-
logout() - 退出系统。
-
substituteUser() - 用另一个用户替换当前会话中的用户。此方法会创建一个新的 UserSession 实例,但会话 ID 不变。
-
getSession() - 获取当前用户会话。
成功登录后,Connection 将 UserSession 对象存储到 VaadinSession
的属性中并设置 SecurityContext
。Connection 对象被绑定到 VaadinSession
,因此无法从非 UI 线程使用它,如果在非 UI 线程调用 login/logout
,则会抛出 IllegalConcurrentAccessException。
通常,登录是通过 AppLoginWindow
界面执行的,该界面支持使用用户名/密码和“记住我”凭据登录。
Connection
的默认实现是 ConnectionImpl
,它将登录委托给 LoginProvider
实例链。LoginProvider
是一个可以处理特定 Credentials
实现的登录模块,它还有一个特殊的 supports()
方法,允许调用者查询它是否支持给定的 Credentials
类型。
标准用户登录过程:
-
用户输入用户名和密码。
-
Web 客户端 block 创建一个
LoginPasswordCredentials
对象,将用户名和密码传递给其构造函数,并使用此凭据调用Connection.login()
方法。 -
Connection
查找对象LoginProvider
对象链。 这种情况下使用的是LoginPasswordLoginProvider
,它支持LoginPasswordCredentials
凭据。LoginPasswordLoginProvider
使用PasswordEncryption
bean 的getPlainHash()
方法散列密码,并调用AuthenticationService.login(Credentials)
。 根据 cuba.checkPasswordOnClient属性设置,它要使用户名和密码调用AuthenticationService.login(Credentials)
方法;或者通过用户名加载User
实体、根据加载的密码哈希验证密码,验证通过后使用TrustedClientCredentials
和 cuba.trustedClientPassword作为可信客户端登录。 -
如果验证成功,则创建的具有活动UserSession的
AuthenticationDetails
实例将被回传给Connection
。 -
Connection
创建一个ClientUserSession
包装器并将其设置到VaadinSession
的属性中。 -
Connection
创建一个SecurityContext
实例并将其设置为AppContext
。 -
Connection
触发StateChangeEvent
,此事件会触发 UI 更新和AppMainWindow
初始化。
所有 LoginProvider
实现必须:
-
使用
Credentials
对象验证用户。 -
使用
AuthenticationService
启动新用户会话或返回另一个活动会话(例如,匿名的)。 -
返回身份验证详细信息,如果无法使用此
Credentials
对象登录用户,则返回空,例如,如果登录提供程序已被禁用或未正确配置。 -
如果出现错误的
Credentials
,则抛出LoginException
或将LoginException
从中间件传递给调用者。
HttpRequestFilter
- bean 的标记接口,这种 bean 将作为 HTTP 过滤器自动被添加到应用程序过滤器链: https://docs.oracle.com/javaee/6/api/javax/servlet/Filter.html 。可以使用它来实现其它形式的身份验证、对 HTTP 请求和响应进行预处理或后处理。
要添加额外的 Filter
, 可以创建 Spring Framework 组件并实现 HttpRequestFilter
接口:
@Component
public class CustomHttpFilter implements HttpRequestFilter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain)
throws IOException, ServletException {
// delegate to the next filter/servlet
chain.doFilter(request, response);
}
@Override
public void destroy() {
}
}
请注意,最简单的实现必须将执行委托给 FilterChain
,否则应用程序将无法工作。默认情况下,作为 HttpRequestFilter
bean 被添加的过滤器将不会收到对 VAADIN
目录和 cuba.web.cubaHttpFilterBypassUrls
app 属性中指定的其它路径的请求。
- 内置登录提供程序
-
平台包含以下
LoginProvider
接口的实现:-
AnonymousLoginProvider
- 为不需登录的用户提供匿名登录。 -
LoginPasswordLoginProvider
-将登录委托给使用LoginPasswordCredentials
的AuthenticationService
。 -
RememberMeLoginProvider
- 将登录委托给使用RememberMeCredentials
的AuthenticationService
。 -
LdapLoginProvider
- 授受LoginPasswordCredentials
参数,使用 LDAP 执行身份验证并将登录委托给使用TrustedClientCredentials
的AuthenticationService
服务。 -
ExternalUserLoginProvider
- 授受ExternalUserCredentials
参数,将登录委托给使用TrustedClientCredentials
的AuthenticationService
服务。可使用提供的用户名执行登录。
所有实现都使用
AuthenticationService.login()
创建一个活动的用户会话。可以使用 Spring Framework 的机制覆盖它们中的任何一个。
-
- 事件
-
Connection
的标准实现 -ConnectionImpl
在登录过程中触发以下应用程序事件:-
BeforeLoginEvent
/AfterLoginEvent
-
LoginFailureEvent
-
UserConnectedEvent
/UserDisconnectedEvent
-
UserSessionStartedEvent
/UserSessionFinishedEvent
-
UserSessionSubstitutedEvent
BeforeLoginEvent
和LoginFailureEvent
的事件处理程序可能抛出LoginException
来取消登录过程或覆盖初始登录失败异常。例如,可以使用
BeforeLoginEvent
实现只允许登录名中包含有公司域名的用户登录 Web 客户端。@Component public class BeforeLoginEventListener { @Order(10) @EventListener protected void onBeforeLogin(BeforeLoginEvent event) throws LoginException { if (event.getCredentials() instanceof LoginPasswordCredentials) { LoginPasswordCredentials loginPassword = (LoginPasswordCredentials) event.getCredentials(); if (loginPassword.getLogin() != null && !loginPassword.getLogin().contains("@company")) { throw new LoginException( "Only users from @company are allowed to login"); } } } }
此外,标准应用程序类 -
DefaultApp
会触发以下事件:-
AppInitializedEvent
- 在App
初始化后触发,每个 HTTP 会话执行一次。 -
AppStartedEvent
- 在以匿名用户身份登录进行第一次请求处理时触发。事件处理器可以使用绑定到App
的Connection
对象来完成用户登录。 -
AppLoggedInEvent
- 用户登录成功时的App
UI 初始化后触发。 -
AppLoggedOutEvent
- 用户注销时的App
UI 初始化后触发。 -
SessionHeartbeatEvent
- 收到来自客户端 Web 浏览器的心跳请求时触发。
AppStartedEvent
可用于使用第三方认证系统实现 SSO 登录,例如 Jasig CAS。通常,它与自定义HttpRequestFilter
bean 一起使用,该 bean 应收集并提供其它身份验证数据。我们假设:如果用户有一个特殊的 cookie 值 -
PROMO_USER
,应用程序将自动登录。@Order(10) @Component public class AppStartedEventListener implements ApplicationListener<AppStartedEvent> { private static final String PROMO_USER_COOKIE = "PROMO_USER"; @Inject private Logger log; @Override public void onApplicationEvent(AppStartedEvent event) { String promoUserLogin = event.getApp().getCookieValue(PROMO_USER_COOKIE); if (promoUserLogin != null) { Connection connection = event.getApp().getConnection(); if (!connection.isAuthenticated()) { try { connection.login(new ExternalUserCredentials(promoUserLogin)); } catch (LoginException e) { log.warn("Unable to login promo user {}: {}", promoUserLogin, e.getMessage()); } finally { event.getApp().removeCookie(PROMO_USER_COOKIE); } } } } }
因此,如果用户拥有“PROMO_USER”cookie 并打开应用程序,它们将自动以
promoUserLogin
身份登录。如果要在登录和 UI 初始化后执行其它操作,可以使用
AppLoggedInEvent
。 需要注意的是,在事件处理程序中必须检查用户是否进行了身份验证,因为所有事件也会对anonymous
用户触发。 -
- 扩展点
-
可以使用以下类型的扩展点扩展登录机制:
-
Connection
- 替换现有的ConnectionImpl
。 -
HttpRequestFilter
- 实现额外的HttpRequestFilter
。 -
LoginProvider
实现 - 实现额外的或替换现有的LoginProvider
。 -
事件 - 为一个可用的事件实现事件处理器。
可以使用 Spring Framework 机制替换现有 bean,例如通过在 web 模块的 Spring XML 配置中注册新 bean。
<bean id="cuba_LoginPasswordLoginProvider" class="com.company.demo.web.CustomLoginProvider"/>
-
- 过时/弃用
-
可以通过设置以下应用程序属性来启用
CubaAuthProvider
接口的自定义实现:cuba.web.externalAuthentication = true cuba.web.externalAuthenticationProviderClass = com.company.sample.web.MyAuthProvider
现已弃用以下组件:
-
CubaAuthProvider
及其实现在兼容模式下可用。使用事件 、LoginProvider
和HttpRequestFilter
代替。 -
LdapAuthProvider
已替换为LdapLoginProvider
,可按这里的描述启用:LDAP 集成 -
IdpAuthProvider
已替换为IdpLoginProvider
,可按 IDP 插件 wiki 的描述启用。
不要使用这些组件。它们会在平台的下一个主版本中被删除。
请使用WEB 登录扩展点代替。
-
3.5.19. 不支持浏览器的界面
如果应用程序不支持某个版本的浏览器,用户将会看到一个带有通知消息的标准界面,通知消息会建议升级浏览器并提供推荐的浏览器列表。
用户只有升级了浏览器才能继续使用应用系统。
可以修改或者本地化默认界面的内容。如果需要本地化该界面,可以在 web 模块的主消息包中使用以下键值:
-
unsupportedPage.captionMessage
– 通知消息标题; -
unsupportedPage.descriptionMessage
– 通知消息描述; -
unsupportedPage.browserListCaption
– 浏览器列表的标题; -
unsupportedPage.chromeMessage
– Chrome 浏览器的信息; -
unsupportedPage.firefoxMessage
– Firefox 浏览器的信息; -
unsupportedPage.safariMessage
– Safari 浏览器的信息; -
unsupportedPage.operaMessage
– Opera 浏览器的信息; -
unsupportedPage.edgeMessage
– Edge 浏览器的信息; -
unsupportedPage.explorerMessage
– Explorer 浏览器的信息。
另外,整个界面也可以用自定义的模板替换:
-
创建一个新的
*.html
文件模板。 -
在
web-app.properties
文件中,使用cuba.web.unsupportedPagePath
应用程序属性设置新模板的路径:cuba.web.unsupportedPagePath = /com/company/sample/web/sys/unsupported-page-template.html
3.6. GUI 历史版本 API
3.6.1. 界面(历史版本)
对于从 v.7.0 开始的新 API,请参阅 界面。 |
要从主菜单或者通过 Java 代码(比如从不同界面的控制器)调用一个界面,这个界面的 XML 描述需要在项目的 screens.xml 文件里注册。用户登录之后打开的默认界面可以通过 cuba.web.defaultScreenId 这个应用程序属性来设置。
应用程序的主菜单内容根据menu.xml文件生成。
3.6.1.1. 界面类型
本章节描述以下几种基本的界面类型:
3.6.1.1.1. 界面子框架
子框架(Frame)是可重用的界面。界面子框架通过 frame XML 元素添加在别的界面中。
子框架的控制器必须扩展 AbstractFrame 类。
在 Studio 里面可以使用 Blank frame 模板创建界面框架。 |
以下是一些界面框架和框架内包含的其它界面交互的规则:
-
框架内的界面组件可以通过“.”来获得引用:
frame_id.component_id
。 -
框架内的界面组件也可以在控制器中通过调用
getComponent(component_id)
方法获得,但是这个方法只有在框架内没有相同 id 的组件才能用。比如,frame 内的组件名称会覆盖界面的组件名称。 -
界面的数据源可以在子界面框架内访问,有三种途径:通过调用
getDsContext().get(ds_id)
方法获得、通过依赖注入获得、通过数据源查询中的ds$ds_id
获得。但是只有在子框架没有声明同名的数据源情况下才能取到(跟组件的情况类似)。 -
从界面上想要获取子框架的数据源,只能通过遍历
getDsContext().getChildren()
集合的方法得到。
界面的提交会促使界面内所有子框架内有更改的 datasource 一起提交。
3.6.1.1.2. 简单界面
简单界面可以用来展示和编辑信息,包括单一实体实例或者实体列表。这种类型的界面只能在应用程序主窗口打开并且使用数据源的核心功能。
简单界面的控制器必须从 AbstractWindow 类继承。
可以在 Studio 使用 Blank screen 模板创建简单界面。 |
3.6.1.1.3. 查找界面
查找界面设计用来选择并且返回实体实例或者实体列表。可视化组件中的 PickerField 和 LookupPickerField 使用的标准 LookupAction 就是调用查找界面来选择相关的实体。
当通过 openLookup()
方法调用查找界面的时候,界面会包含一个面板以及用来选择的一些按钮。当用户选择了一个实例或者多个实例的时候,查找界面调用之前传递给它的处理器函数,从而能将查找结果返回给调用者的代码。当通过 openWindow()
方法或者比如说通过主菜单打开查找界面的时候,用来选择的面板不会显示,查找界面会被有效的转化成一个简单界面。
查找界面的控制器必须从 AbstractLookup 类继承。界面 XML 描述中的 lookupComponent
属性必须指向一个组件(比如表格),从这个组件中选择需要的实体实例作为查找结果。
可以在 Studio 里使用 Entity browser 或者 Entity combined screen 模板来创建实体的查找界面。 |
默认情况下,LookupAction 会使用注册在 screens.xml 文件中的一个查找界面,注册的标识符是 {entity_name}.lookup
或者 {entity_name}.browse
,比如,sales$Customer.lookup
。所以在使用上面提到的组件的时候,确保有个查找界面已经注册。Studio 会使用 {entity_name}.browse
标识符注册浏览界面,所以这些界面会被默认当作查找界面使用。
- 自定义查找界面样式和行为
-
-
需要更改项目中所有查找界面的查找按钮面板(Select 和 Cancel 按钮),可以创建一个界面子框架(frame)并且使用
lookupWindowActions
标识符注册。系统默认的界面框架在/com/haulmont/cuba/gui/lookup-window.actions.xml
。自定义的界面框架必须包含一个链接到lookupSelectAction
行为的按钮(当界面作为查找界面打开时会自动添加这个按钮)。 -
需要在某些特定的界面替换查找按钮面板,只需要在界面中创建一个链接到
lookupSelectAction
行为的按钮,这样的话平台不会添加默认的按钮面板。示例:<layout expand="table"> <hbox> <button id="selectBtn" caption="Select item" action="lookupSelectAction"/> </hbox> <!-- ... --> </layout>
-
需要用自定义的操作替换掉默认的选择动作时,只需要在控制器添加自定义的 action:
@Override public void init(Map<String, Object> params) { addAction(new SelectAction(this) { @Override protected Collection getSelectedItems(LookupComponent lookupComponent) { Set<MyEntity> selected = new HashSet<>(); // ... return selected; } }); }
使用
com.haulmont.cuba.gui.components.SelectAction
作为 action 的基类,如果需要的话,重写里面的方法。
-
3.6.1.1.4. 编辑界面
编辑界面用来展示和编辑实体实例。编辑界面通过需要编辑的实体来做界面初始化,并且包含操作,能把对实体的改动保存到数据库。编辑界面需要通过 openEditor()
方法打开,此方法接收一个实体的实例作为参数。
默认情况下,标准的 CreateAction - 创建和 EditAction - 编辑操作会打开编辑界面,编辑界面使用 {entity_name}.edit
标识符注册在 screens.xml 文件内,比如,sales$Customer.edit
。
编辑界面控制器必须从 AbstractEditor 类继承。
可以在 Studio 里使用 Entity editor 模板来创建实体的编辑界面。 |
编辑界面的 XML 描述中,datasource
属性应当指向一个包含编辑实体实例的数据源。以下这些 XML 中标准的按钮子框架组可以用来展示提交或者撤销操作:
-
editWindowActions
(com/haulmont/cuba/gui/edit-window.actions.xml
文件) – 包含 OK 和 Cancel 按钮。 -
extendedEditWindowActions
(com/haulmont/cuba/gui/extended-edit-window.actions.xml
文件) – 包含 OK & Close,OK 和 Cancel
下列操作需要在编辑界面显式初始化:
-
windowCommitAndClose
(对应于Window.Editor.WINDOW_COMMIT_AND_CLOSE
常量) – 提交改动到数据库并且关闭界面的操作。如果界面有windowCommitAndClose
标识符的可视化组件,则会初始化这个操作。当使用上面提到的extendedEditWindowActions
子框架的时候,这个操作会显示为 OK & Close 按钮。 -
windowCommit
(对应于Window.Editor.WINDOW_COMMIT
常量) – 提交改动到数据库的操作。如果界面没有windowCommitAndClose
,此操作会在提交数据之后关闭界面。如果界面有上面提到的标准子框架,这个操作会显示为 OK 按钮。 -
windowClose
(对应于Window.Editor.WINDOW_CLOSE
常量) – 关闭界面不提交改动。界面总是会初始化这个操作。如果使用上面提到的标准子框架,这个动作显示为 Cancel 按钮。
因此,如果界面包含 editWindowActions
子界面框架,使用 OK 按钮来提交改动并且关闭界面,使用 Cancel 按钮关闭界面不提交改动。如果界面包含 extendedEditWindowActions
子界面框架,使用 OK 按钮只用来提交改动,OK & Close 按钮用来提交改动并且关闭界面,使用 Cancel 按钮关闭界面不提交改动。
除了标准的子界面框架之外,也可以用一些其它的组件来展现界面行为,比如 LinkButton。
3.6.1.1.5. 组合界面
组合界面控制器必须从 EntityCombinedScreen 类继承。
可以在 Studio 里使用 Entity combined screen 模板来创建实体的组合界面。 |
3.6.1.2. 界面 XML 描述
这是历史版本 API。对于 v.7.0 的新 API,请参阅界面 XML 描述。 |
XML 描述是一个 XML 格式的文件,用来描述数据源和界面布局。
XML schema: http://schemas.haulmont.com/cuba/7.0/window.xsd 。
描述文件有如下结构:
window
− 根节点元素
window
的属性:
-
class
− 界面控制器类名。 -
messagesPack
− 界面默认的消息语言包。在控制器中可以通过getMessage()
方法或者在 XML 描述中使用消息键值来获取语言包里面的本地化消息语言,使用消息键值的时候,不需要指定包名。 -
caption
− 窗口标题,可以包含指向上面提到的语言包的一个 消息键值链接,比如:caption="msg://credits"
-
focusComponent
− 一个组件的标识符,当界面展示的时候会默认聚焦到这个组件。 -
lookupComponent
– 查找界面的必须属性;定义一个可视化组件的标识符,通过这个组件选取实体实例。支持以下类型的组件(及其它们的子类):-
Table - 表格
-
Tree - 树形组件
-
LookupField - 下拉框控件
-
PickerField - 选取器控件
-
OptionsGroup - 选项组控件
-
window
的元素:
-
metadataContext
− 这个元素用来初始化界面需要的视图。建议在同一个 views.xml 文件里定义所有的视图,因为所有的视图描述都部署在同一个仓库(repository)中,所以如果视图描述散落在很多个文件中,很难保证视图名称的唯一性。 -
dsContext
− 定义界面的数据源。 -
dialogMode
- 在界面通过对话框的方式打开时,定义窗口的几何属性以及行为。dialogMode
的属性:-
closeable
- 定义对话框是否带有关闭按钮。可选值:true
,false
。 -
closeOnClickOutside
- 当窗口通过模态窗(modal)模式打开时,定义对话框是否可以通过点击窗口之外的区域关闭。可选值:true
,false
。 -
forceDialog
- 设定界面始终需要通过对话框的方式打开,不论调用方代码怎么选取的WindowManager.OpenType
值。可选值:true
,false
。 -
height
- 设置对话框的高度。 -
maximized
- 如果设置为true
,对话框会按照界面大小最大化打开。可选值:true
,false
。 -
modal
- 设定是否按照模态框方式弹出窗口。可选值:true
,false
。 -
positionX
- 设定弹出窗口的左上角位置的x
坐标。 -
positionY
- 设定弹出窗口的左上角位置的y
坐标。 -
resizable
- 设定用户是否可以改变对话框的大小。可选值:true
,false
。 -
width
- 设置对话框的宽度。
示例:
<dialogMode height="600" width="800" positionX="200" positionY="200" forceDialog="true" closeOnClickOutside="false" resizable="true"/>
-
-
actions
– 定义界面的操作列表。 -
timers
– 定义界面的定时器列表。 -
companions
– 定义界面控制器的 companion - 友类列表。companions
的元素:-
web
– 定义 web 模块的友类实现。 -
desktop
– 定义 desktop 模块的友类实现。
这两个元素都有一个
class
属性,用来定义友类。 -
-
layout − 界面布局的根节点元素,是一个具有组件纵向布局的容器。
3.6.1.3. 界面控制器
这是历史版本 API。对于 v.7.0 的新 API,请参阅界面控制器。 |
界面控制器是一个 Java 或者 Groovy 的类,链接到一个界面 XML 描述并且包含界面初始化以及事件处理逻辑。
控制器需要继承下列基类之一:
-
AbstractFrame − 实现界面子框架。
-
AbstractWindow − 实现简单界面。
-
AbstractLookup − 实现查找界面。
-
AbstractEditor − 实现组合界面。
如果界面不需要额外添加处理逻辑,也可以使用基类本身作为控制器 - |
控制器类需要在界面的 XML 描述的 window
根节点元素的 class
属性里注册。
3.6.1.3.1. AbstractFrame
AbstractFrame
是控制器类结构的根节点基类。以下是其主要方法的介绍:
-
init()
在创建了 XML 描述中的所有组件之后,但是在界面显示之前会被调用。init()
接受一组以 map 类型传递的参数,可以用在控制器里面。这些参数可以通过调用界面的方法传递(使用openWindow()
,openLookup()
或者openEditor()
方法),或者在界面注册文件 screens.xml 里面定义。如果需要初始化界面组件,必须实现
init()
方法,示例:@Inject private Table someTable; @Override public void init(Map<String, Object> params) { someTable.addGeneratedColumn("someColumn", new Table.ColumnGenerator<Colour>() { @Override public Component generateCell(Colour entity) { ... } }); }
-
openFrame()
– 根据在 screens.xml 文件中注册的标识符加载一个子界面框架。如果调用者给这个方法传递了一个容器组件的参数,子界面框架会在这个容器内打开。此方法返回子框架控制器。示例:@Inject private BoxLayout container; @Override public void init(Map<String, Object> params) { SomeFrame frame = openFrame(container, "someFrame"); frame.setHeight("100%"); frame.someInitMethod(); }
但这并不是说非要在
openFrame()
方法中带入容器参数,其实也可以先加载子界面框架然后再添加到所需的容器中:@Inject private BoxLayout container; @Override public void init(Map<String, Object> params) { SomeFrame frame = openFrame(null, "someFrame"); frame.setHeight("100%"); frame.someInitMethod(); container.add(frame); }
-
openWindow()
,openLookup()
,openEditor()
– 分别用来打开简单界面、查找界面、以及编辑界面。方法会返回这些界面的控制器。对于对话框模式,
openWindow()
方法可以带参数调用,示例:@Override public void actionPerform(Component component) { openWindow("sec$User.browse", WindowManager.OpenType.DIALOG.width(800).height(300).closeable(true).resizable(true).modal(false)); }
这些参数只有在不与窗口更高优先级的参数冲突的时候才会生效。这些高优先级的参数可以通过界面控制器的 getDialogOptions() 方法设置,或者在界面的 XML 描述中定义:
<dialogMode forceDialog="true" width="300" height="200" closeable="true" modal="true" closeOnClickOutside="true"/>
如果需要在界面关闭之后做一些操作,可以添加
CloseListener
,示例:CustomerEdit editor = openEditor("sales$Customer.edit", customer, WindowManager.OpenType.THIS_TAB); editor.addCloseListener((String actionId) -> { // do something });
只有在打开的窗口通过
Window.COMMIT_ACTION_ID
名称的操作(比如 OK 按钮)关闭的时候才需要使用CloseWithCommitListener
来处理关闭事件:CustomerEdit editor = openEditor("sales$Customer.edit", customer, WindowManager.OpenType.THIS_TAB); editor.addCloseWithCommitListener(() -> { // do something });
-
showMessageDialog()
– 显示消息对话框。
-
showOptionDialog()
– 显示带消息的对话框,并且为用户提供一些功能操作。操作通过 Action 的数组定义,这些操作会作为按钮显示。推荐使用
DialogAction
对象来显示标准按钮,比如 OK,Cancel 或者其它按钮,示例:showOptionDialog("PLease confirm", "Are you sure?", MessageType.CONFIRMATION, new Action[] { new DialogAction(DialogAction.Type.YES) { @Override public void actionPerform(Component component) { // do something } }, new DialogAction(DialogAction.Type.NO) });
-
showNotification()
– 显示弹出消息。
-
showWebPage()
– 在浏览器打开特定网页。
3.6.1.3.2. AbstractWindow
AbstractWindow
是 AbstractFrame 的子类,定义下列方法:
-
getDialogOptions()
– 返回一个DialogOptions - 对话框选项
对象,在界面以对话框模式(WindowManager.OpenType.DIALOG
)打开的时候用来控制窗口的几何属性以及行为。这些选项可以在界面初始化的时候设置,也可以在运行时设置。参考下面的例子。设置宽度和高度:
@Override public void init(Map<String, Object> params) { getDialogOptions().setWidth("480px").setHeight("320px"); }
设置对话框在界面中的位置:
getDialogOptions() .setPositionX(100) .setPositionY(100);
设置对话框可通过点击外部区域关闭:
getDialogOptions().setModal(true).setCloseOnClickOutside(true);
设置对话框非模态(non-modal)打开,并且可改变大小:
@Override public void init(Map<String, Object> params) { getDialogOptions().setModal(false).setResizable(true); }
设置对话框打开时候最大化:
getDialogOptions().setMaximized(true);
设置界面总是按照对话框的方式打开,不论在调用时
WindowManager.OpenType
参数是如何选择:@Override public void init(Map<String, Object> params) { getDialogOptions().setForceDialog(true); }
-
setContentSwitchMode()
- 定义在指定的窗口中 主 tab 标签应当怎样切换标签页:隐藏还是清除界面内容。有三个可选项:
-
DEFAULT
- 切换模式由 TabSheet 模式 cuba.web.managedMainTabSheetMode 应用程序属性定义。 -
HIDE
- 不考虑 TabSheet 的模式,直接隐藏。 -
UNLOAD
- 不考虑 TabSheet 的模式,清除界面内容。
-
-
saveSettings()
- 界面关闭时,保存当前用户对界面的设置到数据库。比如,界面包含一个复选框 showPanel 用来管理某个面板是否可见。在下面的方法中,为复选框创建一个 XML 元素,然后用这个组件的值添加一个
showPanel
属性,最后为当前用户保存 XML 描述的settings
元素内容到数据库:@Inject private CheckBox showPanel; @Override public void saveSettings() { boolean showPanelValue = showPanel.getValue(); Element xmlDescriptor = getSettings().get(showPanel.getId()); xmlDescriptor.addAttribute("showPanel", String.valueOf(showPanelValue)); super.saveSettings(); }
-
applySettings()
- 当界面打开时,为当前用户恢复数据库保存的设置。这个方法可以被重写用来保存自定义设置。比如,在下面的方法中,取到上面例子中复选框的 XML 元素,然后确保需要的属性不是 null,之后将恢复的值设置到复选框上:
@Override public void applySettings(Settings settings) { super.applySettings(settings); Element xmlDescriptor = settings.get(showPanel.getId()); if (xmlDescriptor.attribute("showPanel") != null) { showPanel.setValue(Boolean.parseBoolean(xmlDescriptor.attributeValue("showPanel"))); } }
另一个管理设置的例子就是应用程序内的 Administration - 管理 菜单的标准 Server Log - 服务器日志 界面,它能自动保存和恢复最近打开的日志文件。
-
ready()
- 控制器中可以实现的一个模板方法,用来拦截界面打开动作。当界面全部初始化完毕并且打开之后会调用这个方法。
-
validateAll()
– 验证一个界面。方法默认实现就是对所有实现了Component.Validatable
接口的界面组件调用validate()
方法,然后搜集异常的信息,并且显示相应的消息。如果有任何异常,方法会返回false
,否则返回true
。只有在需要完全重写界面验证过程的情况下,才需要重写这个方法。如果需要补充额外的验证,实现一个特殊的模板方法就足够了 -
postValidate()
。
-
postValidate()
– 可以在控制器实现的模板方法,用来做额外的界面验证。这个方法将验证错误信息保存在传给它的参数ValidationErrors
对象内。之后,这些错误信息会跟标准验证的错误信息一起显示,示例:private Pattern pattern = Pattern.compile("\\d"); @Override protected void postValidate(ValidationErrors errors) { if (getItem().getAddress().getCity() != null) { if (pattern.matcher(getItem().getAddress().getCity()).find()) { errors.add("City name can't contain digits"); } } }
-
showValidationErrors()
- 显示验证错误警告。可以重写这个方法来更改默认的警告行为。通知类型可以通过 cuba.gui.validationNotificationType 应用程序属性来定义。@Override public void showValidationErrors(ValidationErrors errors) { super.showValidationErrors(errors); }
-
close()
– 关闭当前界面。这个方法可以传入字符串参数,这个参数然后传递给
preClose()
模板方法,再传递给CloseListener
监听器。因此,关于窗口关闭原因的信息可以通过发起关闭事件的代码获得。推荐使用这些常量来关闭编辑界面:提交改动后使用Window.COMMIT_ACTION_ID
,不提交改动的话使用Window.CLOSE_ACTION_ID
。如果任何数据源包含没保存的改动,在界面关闭前会弹出窗口提示相关信息。通知的类型可以通过 cuba.gui.useSaveConfirmation 应用程序属性调整。
close()
方法还有一个带有force = true
参数的变体,这个方法可以不调用preClose()
就关闭界面,也不会出现关于未保存信息的对话框消息。如果界面顺利关闭,
close()
方法返回true
。如果关闭的过程中断,则会返回false
。、
-
preClose()
控制器中可以实现的一个模板方法,用来拦截界面关闭动作。这个方法可以接收从close()
方法传递过来的参数。如果关闭的过程中断,
preClose()
方法会返回false
。
-
addBeforeCloseWithCloseButtonListener()
- 当界面通过下列方法关闭的时候添加一个关闭时的监听器:界面的关闭按钮、面包屑(bread crumbs)或者TabSheet
标签的关闭操作(Close - 关闭, Close All - 关闭全部, Close Others - 关闭其它)。如果需要避免用户误操作关闭窗口,可以调用BeforeCloseEvent
事件的preventWindowClose()
方法:addBeforeCloseWithCloseButtonListener(BeforeCloseEvent::preventWindowClose);
-
addBeforeCloseWithShortcutListener
- 当界面通过快捷键方式(比如,Esc
按钮)关闭的时候,添加一个关闭时的监听器。如果需要避免用户误操作关闭窗口,可以调用BeforeCloseEvent
事件的preventWindowClose()
方法:addBeforeCloseWithShortcutListener(BeforeCloseEvent::preventWindowClose);
3.6.1.3.3. AbstractLookup
AbstractLookup
是查找界面控制器的基类,AbstractWindow 的子类。定义了下列方法:
-
setLookupComponent()
– 设置查找组件,用来选择实体实例。作为一条编码规则,用来做选择的组件在 XML 描述中定义,不需要在应用程序代码中调用这个方法。
-
setLookupValidator()
– 为界面设置Window.Lookup.Validator
对象,在返回实体的实例之前,这里的validate()
方法会被平台调用。如果validate()
方法返回false
,查找实体的过程或者窗口关闭的过程会被中断。默认情况不设置这个验证器。
3.6.1.3.4. AbstractEditor
AbstractEditor
是编辑界面控制器的基类,AbstractWindow 的子类。
创建控制器类时,推荐使用需要编辑的实体作为 AbstractEditor
的泛型参数。这样会使 getItem()
和 initNewItem()
方法能操作指定的实体类型,应用程序代码也不需要做额外的类型转换。示例:
public class CustomerEdit extends AbstractEditor<Customer> {
@Override
protected void initNewItem(Customer item) {
...
AbstractEditor
定义了下列方法:
-
getItem()
– 返回编辑的实体实例,通过界面主数据源设置(比如在 XML 描述的根节点元素中通过datasource
属性设置)。如果编辑的实体实例不是新建的,界面打开过程会按照主数据源设置的 view 来重新加载实体的实例。
对
getItem()
返回实例的更改,会在数据源的状态中反映出来,之后会被送到 Middleware 然后提交给数据库。需要注意的是,只有在界面通过
setItem()
初始化之后,getItem()
方法才能返回一个值。在此之前,这个方法只会返回null
,比如在init()
或者initNewItem()
方法里调用的时候。但是,在
init()
方法中,可以通过以下方法取到传递给openEditor()
方法参数的实体实例:@Override public void init(Map<String, Object> params) { Customer item = WindowParams.ITEM.getEntity(params); // do something }
initNewItem()
方法可以接收正确类型的实体作为参数。这两种情况下,获取到的实体实例之后都会被重新加载,除非是新建的。因此,不能对实体做修改,或者将此时的实体保存在某个字段留着将来用。
-
setItem()
– 当窗口通过openEditor()
方式打开的时候,平台会调用这个方法将需要编辑的实体设置到主数据源。调用此方法时,所有的界面组件和数据源都已经创建了,并且控制器的init()
方法也已经执行。如果是要初始化界面的话,推荐使用模板方法
initNewItem()
和postInit()
,而不要重写setItem()
。
-
initNewItem()
– 在设置编辑实体实例到主数据源之前平台会自动调用的模板方法。initNewItem()
方法只能给新创建的实体实例调用。这个方法不会给游离实体调用。如果新实体实例必须在设置到主数据源之前做初始化,可以在控制器实现此方法。示例:@Inject private UserSession userSession; @Override protected void initNewItem(Complaint item) { item.setOpenedBy(userSession.getUser()); item.setStatus(ComplaintStatus.OPENED); }
关于使用
initNewItem()
方法的更复杂的例子可以参阅 cookbook。
-
postInit()
– 在编辑实体实例被设置到主数据源之后马上被平台调用的模板方法。在这个方法中,可以使用getItem()
来获取新建的实体实例或者在界面初始化过程中重新加载的实体。这个方法可以在控制器作为界面初始化的最后一步实现:
@Inject private EntityStates entityStates; @Inject protected EntityDiffViewer diffFrame; @Override protected void postInit() { if (!entityStates.isNew(getItem())) { diffFrame.loadVersions(getItem()); } }
-
commit()
– 验证界面,并且通过 DataSupplier 提交数据改动至 Middleware。如果调用带有
validate = false
参数的方法,提交时不会做验证。建议不要重写此方法,改为使用特定的模板方法 -
postValidate()
,preCommit()
和postCommit()
。
-
commitAndClose()
– 验证界面,提交改动到 Middleware 并且关闭界面。Window.COMMIT_ACTION_ID
的值会传递给preClose()
方法以及注册过的CloseListener
监听器。建议不要重写此方法,改为使用特定的模板方法 -
postValidate()
,preCommit()
和postCommit()
。
-
preCommit()
– 在提交改动的过程中被平台调用的模板方法,在成功验证之后,但是在数据提交到 Middleware 之前。这个方法可以在控制器实现。如果返回值是
false
,提交的过程会被中断,如果是关闭窗口过程(如果调用的commitAndClose()
)中的话,也会被中断。示例:@Override protected boolean preCommit() { if (somethingWentWrong) { notifications.create() .withCaption("Something went wrong") .withType(Notifications.NotificationType.WARNING) .show(); return false; } return true; }
-
postCommit()
– 在提交改动的最后阶段被平台调用的模板方法。方法参数:-
committed
– 如果界面有改动,并且已经提交给 Middleware,设置为true
。 -
close
– 如果界面需要在提交改动之后关闭的话,设置为true
。如果界面没有关闭,此方法的默认实现会展示关于成功提交的信息并且调用
postInit()
。可以在控制器重写此方法,以便在成功提交改动之后做额外操作,比如:
@Inject private Datasource<Driver> driverDs; @Inject private EntitySnapshotService entitySnapshotService; @Override protected boolean postCommit(boolean committed, boolean close) { if (committed) { entitySnapshotService.createSnapshot(driverDs.getItem(), driverDs.getView()); } return super.postCommit(committed, close); }
-
下列图表展示初始化序列过程,以及编辑界面的不同提交改动方法。
- API
-
commit() - commitAndClose() - getItem() - initNewItem() - postCommit() - postInit() - preCommit() - setItem()
3.6.1.3.5. EntityCombinedScreen
EntityCombinedScreen
是组合界面控制器的基类,是 AbstractLookup 的子类。
EntityCombinedScreen
类使用硬编码的标识符查找关键组件,比如表格、字段组或者其它组件。如果给组件做另外不同的命名,需要重写类中保护(protected)的方法并返回自定义的标识符,以便控制器能找到自定义的组件。参考类 JavaDocs 了解细节。
3.6.1.3.6. 界面控制器依赖注入
在界面控制器进行依赖注入可以用来请求有效对象的引用。基于这个目的,要求在控制器内声明一个相应类型的字段,或者写一个带有相应参数类型的访问方法(setter),使用下面注解之一:
-
@Inject
– 最简单的方法,会按照 JavaBeans 规则搜索匹配字段/方法类型以及字段名称的对象用来注入。 -
@Named("someName")
– 显示的定义目标对象的名称。
以下类型可以用来注入到控制器:
-
在 XML 描述中定义的此界面的可视化组件。如果属性的类型是从
Component
类派生的,系统会搜索当前界面中相应名称的组件。 -
在 XML 描述中定义的操作行为 - 参考 操作以及操作接口。
-
在 XML 描述中定义的数据源。如果属性的类型是从
Datasource
派生,系统会搜索当前界面中相应名称的数据源。 -
UserSession
。如果属性的类型是 UserSession,系统会注入当前用户会话的对象。 -
DsContext
。如果属性的类型是DsContext
,系统会注入当前界面的DsContext
。 -
WindowContext
。如果属性的类型是WindowContext
,系统会注入当前界面的WindowContext
。 -
DataSupplier
。如果属性的类型是 DataSupplier,系统会注入相应的实例。 -
任何定义在对应客户端 block 上下文的 bean。包括:
-
客户端引入的 Middleware services
-
WindowConfig
-
ExportDisplay
-
-
如果以上提到的都不合适,并且控制器有友类,如果类型匹配的话当前客户端的友类会被注入。
还可以在控制器内注入传递给 init()
方法的 map 类型的参数,使用 @WindowParam
注解。此注解有 name
属性用来定义参数的名称(map 的键值)以及一个可选的 required
的属性。如果 required = true
并且 map 中没有相应的参数,则会在日志中添加一行 WARNING
的信息。
下面例子注入了传递给控制器 init()
方法的 Job
实体:
@WindowParam(name = "job", required = true)
protected Job job;
3.6.2. 数据源(历史版本)
从 v7.0 开始的新数据 API 请参考 数据组件。 |
数据源为数据感知组件提供数据。
可视化组件本身并不访问 Middleware 中间件:它们从关联的数据源中获得实体实例。还有,如果多个可视化组件需要相同的一组实例,此时一个数据源可以跟多个可视化组件关联工作。
-
当用户在组件中改变一个值的时候,这个新的值会被设置到数据源中实体的对应属性。
-
当实体属性的值在代码中被修改,新的值会展示到可视化组件中。
-
用户输入可以通过两种方式监听,一种是数据源监听器,另一种是组件的值监听器 - 这两个监听器会按照顺序接收到事件。
-
需要在应用代码中读写属性的值,推荐使用数据源,而不是组件本身。以下是一个读取属性值的示例:
@Inject private FieldGroup fieldGroup; @Inject private Datasource<Order> orderDs; @Named("fieldGroup.customer") private PickerField customerField; public void init(Map<String, Object> params){ Customer customer; // Get customer from component: not for common use Component component = fieldGroup.getFieldNN("customer").getComponentNN(); customer = ((HasValue)component).getValue(); // Get customer from component customer = customerField.getValue(); // Get customer from datasource: recommended customer = orderDs.getItem().getCustomer(); }
从这个例子可以看出,通过组件获取实体属性值并不是那么直接。在第一个通过组件获取值的例子中,除了需要做类型转换之外,还需要通过一个字符串来指定 FieldGroup 中的
id
。第二个例子更加安全和直接,但是需要知道注入的控件的准确类型。最后一个例子就最简单了。如果实例是从数据源通过getItem()
方法获取到,则可以直接读取和修改里面的属性值。
数据源也会跟踪内部包含的实体的改动,之后可以将改动后的实体实例发送回中间件从而保存在数据库。
典型的场景中,一个可视化组件通常跟数据源中实体的一个直接属性进行绑定。比如上面的例子中,组件绑定到 |
以下介绍数据源的基础接口。
-
Datasource 是一个简单的数据源,用来绑定一个实体实例。实例通过
setItem()
方法设置,通过getItem()
方法访问。DatasourceImpl
类是这种数据源的标准实现,这个类会用在比如说实体的编辑界面,作为主数据源。 -
CollectionDatasource 是一个绑定实体实例集合的数据源。数据集合会通过调用
refresh()
方法加载,实例的主键可以通过getItemIds()
方法访问。setItem()
方法只能设置集合中“当前”的实例,并且通过getItem()
方法获取这个实例(比如,跟当前选中的表格中一行对应的实例)。根据具体实现决定加载数据集合的方式。最典型的方式是通过 DataManager 从 Middleware 加载;这样的话,使用
setQuery()
,setQueryFilter()
来组建 JPQL 查询。CollectionDatasourceImpl
类是这种数据源的标准实现,使用在带有实体列表的界面。-
GroupDatasource 是
CollectionDatasource
的子类型,用来跟 GroupTable 组件一起工作。这种数据源的标准实现是
GroupDatasourceImpl
类。 -
HierarchicalDatasource 是
CollectionDatasource
的子类型,用来跟 Tree 和 TreeTable 组件一起工作。这种数据源的标准实现是
HierarchicalDatasourceImpl
类。
-
-
NestedDatasource 是一个用来绑定实体中关联到实体的属性加载对应的实例的数据源。这样的话,那个绑定父实体的数据源可以通过
getMaster()
方法访问;包含这个数据源对应实例的那个父属性的元属性可以通过getProperty()
方法访问。比如一个
Order
实例包含了指向Customer
实例的引用,Order
实例通过dsOrder
数据源绑定。然后,如需绑定Customer
实例到可视化组件,只需创建以dsOrder
为父数据源的NestedDatasource
,并且创建 meta property 指向Order.customer
属性。-
PropertyDatasource 是
NestedDatasource
的子类型,用来跟单实例或者没有嵌套关系的关联实体集合绑定。标准实现:跟单实例绑定 -
PropertyDatasourceImpl
。跟集合绑定 -CollectionPropertyDatasourceImpl
,GroupPropertyDatasourceImpl
,HierarchicalPropertyDatasourceImpl
。跟集合绑定的数据源同时还实现了CollectionDatasource
接口,但是CollectionDatasource
接口中一些不相关、不支持的方法比如setQuery()
会直接抛出UnsupportedOperationException
异常。 -
EmbeddedDatasource 是
NestedDatasource
的子类型,包含了一个嵌套实体的实例。标准实现是
EmbeddedDatasourceImpl
类。
-
-
RuntimePropsDatasource 是个特殊的数据源,用来操作实体的动态属性。
一般情况下,数据源在界面描述文件的 dsContext
部分声明。
- CollectionDatasource 自动刷新
-
当界面打开时,连接到集合数据源的可视化组件促使数据源加载数据。结果会使得表格在界面打开之后马上就能显示数据而不需要用户的任何特定的操作。如果需要阻止数据源集合的自动加载,可以设置界面参数
DISABLE_AUTO_REFRESH
为true
,一种是在界面的init()
方法中设置,另一种是通过调用者代码传递。这个界面参数定义在WindowParams
枚举类型内,所以可以通过下面方式设置:@Override public void init(Map<String, Object> params) { WindowParams.DISABLE_AUTO_REFRESH.set(params, true); }
在这种情况下,界面的集合数据源只有当它们的
refresh()
方法被调用时才会加载数据。可以通过应用代码调用或者当用户在过滤器组件点击 Search 按钮时触发。
3.6.2.1. 创建数据源
数据源对象可以通过两种方式创建,一种是在界面的 XML 描述中通过声明式的方式,另一种是在界面控制器通过编程的方式创建。通常情况下,创建的数据源默认会使用其标准实现,但是如果需要的话,也可以通过继承标准实现类来自定义数据源。
3.6.2.1.1. 声明式创建
典型的情况下,数据源声明在界面描述文件的 dsContext
元素中。根据声明元素相对位置的不同,可以创建两种类型的数据源:
-
如果元素直接落在了
dsContext
的范围内,比如一个普通的Datasource
或者CollectionDatasource
,会创建一个能独立加载实体或者实体集合的数据源; -
如果元素落在了其它数据源的元素内,则会创建一个
NestedDatasource
- 嵌套的数据源,它是外层数据源的子数据源。
下面为声明一个数据源的示例:
<dsContext>
<datasource id="carDs" class="com.haulmont.sample.entity.Car" view="carEdit">
<collectionDatasource id="allocationsDs" property="driverAllocations"/>
<collectionDatasource id="repairsDs" property="repairs"/>
</datasource>
<collectionDatasource id="colorsDs" class="com.haulmont.sample.entity.Color" view="_local">
<query>
<![CDATA[select c from sample$Color c order by c.name]]>
</query>
</collectionDatasource>
</dsContext>
在上述例子中,carDs
包含一个实体实例 Car
,其中嵌套 allocationsDs
和 repairsDs
,分别指向 Car.driverAllocations
和 Car.repairs
两个关联属性。Car
实例和其相关实体都由外部调用时设置到数据源中。如果当前界面为编辑界面,上述设置在界面打开时会自动设置。colorsDs
还包含指向 Color
实体的集合数据源,这个数据源则是由特定 JPQL查询语句使用 _local 视图设置。
以下是 XML 描述:
dsContext
– 根节点。
dsContext
元素:
-
datasource
– 定义包含一个实体示例的数据源。属性:
-
collectionDatasource
– 指对应实例集合的数据源。collectionDatasource
属性:-
refreshMode
– 数据源更新模式,默认为ALWAYS
。如果设置为NEVER
,当调用refresh()
时,数据源不载入数据,只是将状态置为Datasource.State.VALID
,通知监听器和需要排序的实例。当你在代码中使用预先载入或者创建好的实体设置CollectionDatasource
时,NEVER
模式会有用。例如:@Override public void init(Map<String, Object> params) { Set<Customer> entities = (Set<Customer>) params.get("customers"); for (Customer entity : entities) { customersDs.includeItem(entity); } customersDs.refresh(); }
-
softDeletion
– 设置为 false 时,载入数据时禁用软删除模式,即,被删除的示例也会被载入。默认值为true
。
collectionDatasource
元素:-
query
– 载入实体的查询语句。
-
-
groupDatasource
– 与collectionDatasource
完全类似,但是会创建适合与 GroupTable 组件结合使用的数据源。 -
hierarchicalDatasource
– 类似collectionDatasource
,但是会创建适合与 Tree 、 TreeTable 组件结合使用的数据源。hierarchyProperty
为特定属性,指定基于哪个属性组建 hierarchy 层级树数据结构。
如上所述,数据源对应类需要由 XML 元素明确指定,以及通过 XML 元素的相互关系确定。不过如果需要定制化数据源,可以通过 datasourceClass
指定。
3.6.2.1.2. 编程方式创建
如果需要在 Java 代码中创建数据源,推荐使用一个特殊的类 - DsBuilder
。
DsBuilder
是通过流式接口调用链的方式传递参数的。如果设置了 master
和 property
参数,会创建 NestedDatasource
,否则,创建 Datasource
或者 CollectionDatasource
。
示例:
CollectionDatasource ds = new DsBuilder(getDsContext())
.setJavaClass(Order.class)
.setViewName(View.LOCAL)
.setId("ordersDs")
.buildCollectionDatasource();
3.6.2.1.3. 自定义实现类
如果需要实现使用自定义方法来加载实体,可以创建自定义的数据源类,从 CustomCollectionDatasource
,CustomGroupDatasource
, 或者 CustomHierarchicalDatasource
继承,然后实现 getEntities()
方法。
示例:
public class MyDatasource extends CustomCollectionDatasource<SomeEntity, UUID> {
private SomeService someService = AppBeans.get(SomeService.NAME);
@Override
protected Collection<SomeEntity> getEntities(Map<String, Object> params) {
return someService.getEntities();
}
}
还可以通过声明的方式创建自定义数据源,在数据源的 XML 元素中,通过 datasourceClass
属性指定自定义的类名即可。通过 DsBuilder
类采用编程的方式创建的话,使用 setDsClass()
方法来指定自定义类,或者在 build*()
方法中将类以参数的形式传入。
3.6.2.2. 集合数据源查询
CollectionDatasourceImpl
类及其子类 GroupDatasourceImpl
和 HierarchicalDatasourceImpl
都是数据源的标准实现类,用来处理实体实例的集合。这些数据源发送 JPQL 查询语句给 DataManager
来加载数据。下面介绍这些查询语句的格式。
3.6.2.2.1. 返回值
一个查询语句需要返回在创建数据源时指定类型的实体。在以声明式的方式创建数据源时,返回实体的类型通过 XML 元素的 class
属性指定;如果是使用了 DsBuilder
以编程的方式创建,那么通过 setJavaClass()
或者 setMetaClass()
指定。
比如,Customer
实体的数据源的查询语句:
select c from sales$Customer c
或者
select o.customer from sales$Order o
不能使用返回单个属性或者属性聚合值(比如 sum,avg,max 等)的查询语句,示例:
select c.id, c.name from sales$Customer c /* 无效 – 返回了单个字段而不是整个 Customer 对象 */
如果需要执行返回值是纯数值(scalar value)或者属性聚合值(aggregates)的查询语句,并且将返回值通过标准数据绑定显示在可视化组件上,可以使用值数据源。
3.6.2.2.2. 查询参数
数据源中的 JPQL 查询语句可能包含几种不同类型的参数。参数类型是通过参数名称的前缀决定的,参数名称中 $ 符号之前的部分就是名称前缀,下面针对不同的前缀分别介绍一下 $ 符号之后部分的理解。
-
ds
前缀这个参数的值是在同一个
DsContext
注册的其它数据源的数据。示例:<collectionDatasource id="customersDs" class="com.sample.sales.entity.Customer" view="_local"> <query> <![CDATA[select c from sales$Customer c]]> </query> </collectionDatasource> <collectionDatasource id="ordersDs" class="com.sample.sales.entity.Order" view="_local"> <query> <![CDATA[select o from sales$Order o where o.customer.id = :ds$customersDs]]> </query> </collectionDatasource>
上面这个例子中,
ordersDs
数据源的查询参数是customersDs
数据源当前加载的实体实例。如果使用了
ds
开头的参数,会自动创建数据源之间的依赖关系。因此能在参数变化了的情况下数据源自动更新。比如在上面的例子中,如果选择的 Customer 改变了,Order 的列表会自动更新。需要注意的是,在上面这个带参数的查询中,等号左边的部分是
o.customer.id
标识符的值,右边部分则是customersDs
数据源中的Customer
实例。这个等式能成立是因为在 Middleware 运行这个查询的时候,Query 接口的实现类会在给查询参数赋值的时候自动将实体的实例替换成实体的 ID。也可以在 $ 符号后面的数据源中使用实体关系图(entity graph)中的路径来指定一个深层的属性(直接用这个属性的值),示例:
<query> <![CDATA[select o from sales$Order o where o.customer.id = :ds$customersDs.id]]> </query>
或者
<query> <![CDATA[select o from sales$Order o where o.tagName = :ds$customersDs.group.tagName]]> </query>
-
custom
前缀参数值会从传给数据源
refresh()
方法的Map<String, Object>
对象中获取。示例:<collectionDatasource id="ordersDs" class="com.sample.sales.entity.Order" view="_local"> <query> <![CDATA[select o from sales$Order o where o.number = :custom$number]]> </query> </collectionDatasource>
ordersDs.refresh(ParamsMap.of("number", "1"));
如果需要的话,这里也会将实体实例转成标识符,这个机制跟
ds
前缀里面描述的类似。但是这里不支持实体关系图路径。
-
param
前缀参数值从传递给界面控制器
init()
方法的Map<String, Object>
对象中获取。示例:<query> <![CDATA[select e from sales$Order e where e.customer = :param$customer]]> </query>
openWindow("sales$Order.lookup", WindowManager.OpenType.DIALOG, ParamsMap.of("customer", customersTable.getSingleSelected()));
如果需要的话,这里也会将实体实例转成标识符,这个机制跟
ds
前缀里面描述的类似。这里也支持使用实体关系图路径的参数名称。
-
component
前缀使用一个可视化组件当前的值作为参数值,通过参数名称来定义组件路径。示例:
<query> <![CDATA[select o from sales$Order o where o.number = :component$filter.orderNumberField]]> </query>
组件的路径需要包含所有嵌套的界面子框架。
如果需要的话,这里也会将实体实例转成标识符,这个机制跟
ds
前缀里面描述的类似。这里也支持使用实体关系图路径的参数名称,但是实体属性的路径要继续添加在组件路径之后。数据源不会因为组件的值改变而自动刷新。
-
session
前缀从用户会话的属性中获取跟参数名称相同的属性值作为参数值。
通过
UserSession.getAttribute()
方法获取这个值,所以会话中预定义的名称都支持。-
userId
– 当前注册用户或者被替代用户的 ID; -
userLogin
– 当前注册用户或者被替代用户的用户名(英文小写)。
示例:
<query> <![CDATA[select o from sales$Order o where o.createdBy = :session$userLogin]]> </query>
如果需要的话,这里也会将实体实例转成标识符,这个机制跟
ds
前缀里面描述的类似。但是这里不支持实体关系图路径。 -
3.6.2.2.3. 查询条件过滤
根据用户输入的条件不同,可以在运行时改变数据源的查询结果。因而可以有效的在数据库级别做数据过滤。
提供此功能最简单的方法就是将数据源连接到一个特殊的可视化组件上:过滤器控件。
如果因为某些原因,全局的过滤器不太合适,可以在查询语句的文本中嵌入一个特殊的 XML 标记。通过这个标记可以根据用户在界面的可视化组件中输入的值进行过滤。
此过滤器中可以使用以下 XML 元素:
-
filter
– 过滤器的根节点元素。这个元素只能直接包含一个条件-
and
,or
– 逻辑条件,可以包含任意数量的其它条件和语句。 -
c
– JPQL 条件,会被添加在查询语句的where
部分。如果此查询语句不包含where
从句,则会被添加在第一个条件前面。可以通过一个可选的join
属性来指定需要关联查询的实体,join
属性的值会被原封不懂的添加到查询的主实体之后,所以join
属性的内容需要包含必要的 join 关键字或者逗号。
-
条件和语句只有在相应的参数有值的时候才会被添加到最终形成的查询语句中,比如当这些值不是 null
的时候。
示例:
<query>
<![CDATA[select distinct d from app$GeneralDoc d]]>
<filter>
<or>
<and>
<c join=", app$DocRole dr">dr.doc.id = d.id and d.processState = :custom$state</c>
<c>d.barCode like :component$barCodeFilterField</c>
</and>
<c join=", app$DocRole dr">dr.doc.id = d.id and dr.user.id = :custom$initiator</c>
</or>
</filter>
</query>
上面的例子中,如果给数据源的 refresh()
方法传递了 state
和 initiator
参数,并且 barCodeFilterField
这个可视化组件也有值,那么组成的查询语句会是这样:
select distinct d from app$GeneralDoc d, app$DocRole dr
where
(
(dr.doc.id = d.id and d.processState = :custom$state)
and
(d.barCode like :component$barCodeFilterField)
)
or
(dr.doc.id = d.id and dr.user.id = :custom$initiator)
但是如果 barCodeFilterField
组件是空的,并且只有 initiator
参数传给了 refresh()
方法,那么组成的语句会是这样:
select distinct d from app$GeneralDoc d, app$DocRole dr
where
(dr.doc.id = d.id and dr.user.id = :custom$initiator)
3.6.2.2.4. 不区分大小写查找
在数据源中可以使用 JPQL 查询语句的一个特殊功能,在 Middleware 层级的 Query 接口中有描述:可以使用 (?i)
前缀来轻松创建大小写不敏感的包含任意子串的查询条件。但是由于查询的值通常是显式传入的,所以会有以下不同:
-
(?i)
前缀需要放置在参数名称之前而不是放置在参数值内。 -
参数值会被自动转化成小写。
-
如果参数值不包含
%
字符,则会在参数值的前后加上%
字符。
以下示例介绍如何处理下面这个查询语句:
select c from sales$Customer c where c.name like :(?i)component$customerNameField
从 customerNameField
组件拿到的参数会被自动转化成小写,并且前后放置 %
字符,然后在数据库会执行 lower(C.NAME) like ?
这样的 SQL 查询语句。
注意在这种情况下,按照 NAME
字段在数据中创建的索引将不会起作用。
3.6.2.3. 值数据源
通过值数据源可以执行返回纯数值或者数值聚合(aggregates)的查询语句。比如,可以为 customer 做一些数据统计:
select o.customer, sum(o.amount) from demo$Order o group by o.customer
值数据源通过称为 KeyValueEntity
的特殊类型的实体跟其它实体交互。这种类型的实体可以在运行时包含任意数量的属性。所以在上面的例子中,KeyValueEntity
实例会包含两个属性:第一个是 Customer
类型的属性,第二个是 BigDecimal
类型的属性。
值数据源的实现类继承了其它广泛使用的集合数据源类,并且实现了一个特殊的接口:ValueDatasource
。下面这个图展示了值数据源的实现类以及它们的基类:
ValueDatasource
接口声明了以下方法:
-
addProperty()
- 由于这个数据源可以返回带有任意数量属性的实体,可以通过此方法添加期待返回的属性。这个方法接收属性的名称和对应的类型作为参数,类型可以用数据类型或者 Java 类表示。如果是 Java 类的话,那么要求这个类必须是实体类或者是一种数据类型支持的类。 -
setIdName()
是一个可选调用的方法,通过这个方法来定义返回的实体中作为主键的属性。也就是说,数据源中返回的KeyValueEntity
实例会用这个方法指定的属性作为唯一标识符。否则的话,KeyValueEntity
实例会用随机的 UUID 做主键。 -
getMetaClass()
返回一个动态生成的MetaClass
接口的实现对象,用来表示当前KeyValueEntity
实例的元数据。这些元数据是通过之前调用的addProperty()
来定义的。
值数据源可以在 XML 描述中声明式的使用。对应不同的实现类,有三中 XML 元素:
-
valueCollectionDatasource
-
valueGroupDatasource
-
valueHierarchicalDatasource
值数据源的 XML 定义必须包含 properties
元素,用来定义数据源中包含的 KeyValueEntity
实例的属性(参考上面提到的 addProperty()
方法)。property
元素的顺序需要按照查询语句返回值的顺序排列。比如,在下面的定义中,customer
属性会从 o.customer
列取得值,sum
属性会从 sum(o.amount)
列取得值:
<dsContext>
<valueCollectionDatasource id="salesDs">
<query>
<![CDATA[select o.customer, sum(o.amount) from demo$Order o group by o.customer]]>
</query>
<properties>
<property class="com.company.demo.entity.Customer" name="customer"/>
<property datatype="decimal" name="sum"/>
</properties>
</valueCollectionDatasource>
</dsContext>
值数据源设计只能用来读取数据,因为 KeyValueEntity
并不是可持久化实体,不能通过标准的持久化机制保存到数据库。
可以手动创建值数据源或者在 Studio 中通过 Screen designer 界面的 Datasources 标签页创建。
通过 Properties 编辑器可以创建针对某种数据类型或者 Java 类的数据源属性。
3.6.2.4. 数据源监听器
通过数据源监听器接收数据源及其包含实体的状态变化的消息通知。
一共有四种类型的监听器。其中三个,ItemPropertyChangeListener
,ItemChangeListener
和 StateChangeListener
都是定义在 Datasource
接口中的,可以在任何数据源使用。CollectionChangeListener
定义在 CollectionDatasource
里,只能在集合数据源使用。
跟 GUI 的 ValueChangeListener 相比,数据源的监听器在界面的生命周期之上提供了更好的控制,建议在界面有绑定数据源的可视化组件的情况下使用。
使用数据源监听器的示例:
public class EmployeeBrowse extends AbstractLookup {
private Logger log = LoggerFactory.getLogger(EmployeeBrowse.class);
@Inject
private CollectionDatasource<Employee, UUID> employeesDs;
@Override
public void init(Map<String, Object> params) {
employeesDs.addItemPropertyChangeListener(event -> {
log.info("Property {} of {} has been changed from {} to {}",
event.getProperty(), event.getItem(), event.getPrevValue(), event.getValue());
});
employeesDs.addStateChangeListener(event -> {
log.info("State of {} has been changed from {} to {}",
event.getDs(), event.getPrevState(), event.getState());
});
employeesDs.addItemChangeListener(event -> {
log.info("Datasource {} item has been changed from {} to {}",
event.getDs(), event.getPrevItem(), event.getItem());
});
employeesDs.addCollectionChangeListener(event -> {
log.info("Datasource {} content has been changed due to {}",
event.getDs(), event.getOperation());
});
}
}
以下介绍上面用到的监听器接口:
-
ItemPropertyChangeListener
通过Datasource.addItemPropertyChangeListener()
方法添加。当数据源包含的实体的一个属性值发生改变的时候,会触发这个监听。可以通过传递给监听器的 event 对象获取实体本身的实例、改变的属性名称以及该属性的新旧值。ItemPropertyChangeListener
可以对通过界面组件修改实体内容而引起变化的情况作出反应,比如,当用户修改了文本输入框的内容。 -
ItemChangeListener
通过Datasource.addItemChangeListener()
方法添加。当通过Datasource.getItem()
方法返回的选中的实体发生改变时触发。对于
Datasource
的情况,当另外一个实例(或者null
)通过setItem()
方法赋值给数据源的时候会触发此事件。对于
CollectionDatasource
的情况,当在关联的可视化组件中,选中的元素变化的时候会触发此事件。比如,可以是选中的表格的一行,树的一个节点或者下拉列表中的一个元素。 -
StateChangeListener
通过Datasource.addStateChangeListener()
方法添加。当数据源的状态发生变化时触发。数据源的状态可以是Datasource.State
枚举类型对应的三种状态之一:-
NOT_INITIALIZED
– 数据源刚被创建。 -
INVALID
– 数据源关联的整个 DsContext 刚创建。 -
VALID
– 数据源可用状态,此时,Datasource
包含了一个实体或者 null,CollectionDatasource
则是包含了一组实体实例或者一个空的集合。
对于复杂的编辑界面来说,接收数据源状态变化的消息通知可能很重要,因为复杂的编辑界面中通常包含了好几个界面子框架,所以有时候很难跟踪到设置编辑实体到数据源的时刻。在这种情况下,就可以用
StateChangeListener
来做界面中某些元素的延时初始化:employeesDs.addStateChangeListener(event -> { if (event.getState() == Datasource.State.VALID) initDataTypeColumn(); });
-
-
CollectionChangeListener
通过CollectionDatasource.addCollectionChangeListener()
方法添加。当数据源中保存的实体集合发生变化的时候触发。event 对象提供getOperation()
方法返回CollectionDatasource.Operation
类型的值:REFRESH - 刷新
,CLEAR - 清空
,ADD - 添加
,REMOVE - 删除
,UPDATE - 更新
。这些值反映了引起集合变化的操作。
3.6.2.5. DsContext
所有通过声明式方法创建的数据源都在界面的 DsContext
对象中注册。DsContext
的引用可以通过在界面控制器中调用 getDsContext()
方法获得,也可以通过 界面控制器依赖注入 获得。
DsContext
是为以下任务设计的:
-
组织数据源的依赖关系,当在一个数据源中设置某个记录(比如,通过
setItem()
方法更改“当前”实例)的时候,引起了另一个关联的数据源的改动。通过这些数据源之间的依赖关系,可以组织界面中可视化组件的主从(master-detail)关系数据源之间的依赖关系通过使用带有
ds$
前缀的查询参数来组织。 -
收集所有修改了的实体实例,然后通过一次单一的调用
DataManager.commit()
将数据提交给 Middleware,比如,可以通过这种方式在单一的数据库事务中保存所有数据更改。举例说明,假设用户可以在某个界面上编辑
Order
实体以及属于Order
的一组OrderLine
实体。Order
实体在Datasource
中,OrderLine
集合在一个嵌套的CollectionDatasource
数据源中,这个嵌套的数据源通过Order.lines
属性创建。如果用户更改了
Order
的某些属性并且创建了一个OrderLine
的新实例,接下来,当界面的改动提交给 DataManager 的时候,两个实体(改动的Order
和新的OrderLine
)会被同时发送给 Middleware。之后,这两个实体会一起被合并到同一个持久化上下文,最后,在数据库事务提交的时候再被保存到数据库。这样的话可以不需要在 ORM 层指定 cascade 参数,而且也避免了在 @OneToMany 注解描述中提到的问题。提交数据库的事务之后,
DsContext
会从 Middleware 收到一组保存到数据库的对象实例(如果是乐观锁的情况,至少这些实体的version
属性会增加),然后将这些收到的实例设置到数据源,替换旧的实体。因此,可以在提交改动之后马上在数据源使用最新的实体实例而不需要再次向 Middleware 和数据库发起额外的刷新请求。 -
声明两个监听器:
BeforeCommitListener
和AfterCommitListener
。这两个监听器分别接收提交实体改动之前和之后的消息通知。通过BeforeCommitListener
可以添加实体集合到 DataManager 然后跟需要提交的数据在一个事务提交。在数据库事务提交之后,可以通过AfterCommitListener
监听器来获得DataManager
返回的提交之后的保存的实体。这个机制在某些时候很有用,比如一些实体,虽然跟界面元素绑定,但是不受数据源的控制,而且在界面控制器创建和修改。比如,FileUploadField 这个可视化组件,当上传文件完成之后,创建了一个
FileDescriptor
的实例,就可以通过这种机制在BeforeCommitListener
添加到CommitContext
跟其它的界面元素一起在提交到数据库。在下面的例子中,当界面提交的时候,一个
Customer
的新实例会被发送到 Middleware 然后跟其它修改过的实体一起被提交到数据库:protected Customer customer; protected void createNewCustomer() { customer = metadata.create(Customer.class); customer.setName("John Doe"); } @Override public void init(Map<String, Object> params) { getDsContext().addBeforeCommitListener(context -> { if (customer != null) context.getCommitInstances().add(customer); } }
3.6.2.6. DataSupplier
DataSupplier
– 接口,数据源通过这个接口访问到 Middleware 以便加载和保存实体。接口的标准实现只是简单的做了 DataManager 的代理。界面可以在 window
元素的 dataSupplier
属性定义它自己的 DataSupplier
实现类。
DataSupplier
的引用可以通过注入的方式或者通过 DsContext
或 Datasource
实例获得。这两种方式下,如果有自定义的实现类,则会使用自定义类。
3.6.3. 对话框消息和通知消息(历史版本)
对话框消息和通知消息可以用来为用户呈现消息。
对话框消息有标题、关闭按钮并且总是在应用程序主窗口的中间展示。通知消息可以显示在窗口中间或者角落,并且能自动消失。
3.6.3.1. 对话框消息
对 v.7.0 之后的新版本 API,请参阅 对话框消息。 |
- 通用对话框
-
通用对话框可以通过
Frame
接口的showMessageDialog()
和showOptionDialog()
方法来调用。由于界面控制器都实现了这个接口,所以可以直接在界面控制器调用这些方法。-
showMessageDialog()
用来展示一条消息,此方法有下列参数:-
title
– 窗口标题 -
message
- 消息内容。对于 HTML 类型的消息,可以使用 HTML 标签来格式化消息内容。使用 HTML 时,确保对数据库取出的数据进行转义保护,避免 web 客户端进行代码注入。在非 HTML 格式的消息中可以使用\n
来换行。 -
messageType
– 消息类型。可能的类型:-
CONFIRMATION
、CONFIRMATION_HTML
– 确认窗口。 -
WARNING
、WARNING_HTML
– 警告窗口消息类型的不同只反映在桌面客户端。
也可以通过参数设置消息类型:
-
width
- 窗口宽度。 -
modal
- 窗口是否模态弹出。 -
maximized
- 对话框是否最大化到整个界面。 -
closeOnClickOutside
- 对话框是否可以通过点击界面对话框外面的部分进行关闭。显示对话框消息示例:
showMessageDialog("Warning", "Something is wrong", MessageType.WARNING.modal(true).closeOnClickOutside(true));
-
-
-
-
showOptionDialog()
用来展示消息以及一些用户可以操作的按钮。除了上面提到的showMessageDialog()
的参数外,这个方法还可以接收一个 action 的数组或者列表,并且会为每个 action 创建一个按钮。当按钮点击后,窗口调用相应操作的actionPerform()
方法然后关闭。对于采用标准名称和图标的按钮来说,使用匿名类继承
DialogAction
很方便,支持使用DialogAction.Type
枚举类型定义的五种动作:OK
、CANCEL
、YES
、NO
、CLOSE
。相应的按钮名称从主语言包中取得。下面这个例子是一个有
Yes
和No
按钮的消息对话框,并且从语言包中获取到当前界面的标题和消息文本:showOptionDialog( getMessage("confirmCopy.title"), getMessage("confirmCopy.msg"), MessageType.CONFIRMATION, new Action[] { new DialogAction(DialogAction.Type.YES, Status.PRIMARY).withHandler(e -> copySettings()), new DialogAction(DialogAction.Type.NO, Status.NORMAL) } );
DialogAction
的Status
参数用来给动作的按钮设置特殊的显示样式。Status.PRIMARY
会使相应的按钮高亮并且被选中。Status
参数也可以省去,这样的话会默认的高亮样式。如果给showOptionDialog
传递了多个Status.PRIMARY
的操作,只有第一个动作的按钮会被设置成cuba-primary-action
样式并且被选中。
-
- 文件上传对话框
-
使用
FileUploadDialog
窗口来提供上传文件到临时存储的基本功能。这个窗口包含了一个可以投放文件的区域,可以通过拖拽的方式从浏览器外将文件投放到指定区域进行上传,同时也提供了一个上传文件的按钮。上传窗口是通过
openWindow()
方法打开的,当上传成功的时候,窗口关闭会返回COMMIT_ACTION_ID
。可以通过CloseListener
或者CloseWithCommitListener
监听器来跟踪窗口的关闭动作,然后用getFileId()
和getFileName()
方法来取到上传文件的 UUID 和名称。之后可以创建一个FileDescriptor
对象用来作为这个文件在数据模型层的引用,可以用这个对象来实现其它业务逻辑。FileUploadDialog dialog = (FileUploadDialog) openWindow("fileUploadDialog", OpenType.DIALOG); dialog.addCloseWithCommitListener(() -> { UUID fileId = dialog.getFileId(); String fileName = dialog.getFileName(); FileDescriptor fileDescriptor = fileUploadingAPI.getFileDescriptor(fileId, fileName); // your logic here });
3.6.3.2. 通知消息
对 v.7.0 之后的新版本 API,请参阅 通知消息。 |
通知消息可以通过 Frame
接口的 showNotification()
方法来调用。由于界面控制器都实现了这个接口,所以可以直接在界面控制器调用此方法。
showNotification()
方法有下列参数:
-
caption
- 通知文本。对于 HTML 类型的消息,可以使用 HTML 标签来格式化消息内容。使用 HTML 时,确保对数据库取出的数据进行转义保护,避免 web 客户端进行代码注入。在非 HTML 格式的消息中可以使用\n
来换行。 -
description
– 在 caption 下显示的一条可选的描述信息。也可以使用 HTML 和非 HTML 格式。 -
type
– 消息类型。可能的类型:-
TRAY
,TRAY_HTML
- 在界面右下角显示通知消息,之后会自动消失。 -
HUMANIZED
,HUMANIZED_HTML
– 显示在界面中间的标准通知消息,之后会自动消失。 -
WARNING
,WARNING_HTML
– 警告通知消息,点击时消失。 -
ERROR
,ERROR_HTML
– 错误通知消息,点击时消失。
-
显示通知消息示例:
showNotification(getMessage("selectBook.text"), NotificationType.HUMANIZED);
showNotification("Validation error", "<b>Date</b> is incorrect", NotificationType.TRAY_HTML);
3.6.4. 集合的标准行为(历史版本)
对于 ListComponent
的继承者们(Table,GroupTable,TreeTable 和 Tree),标准的行为是通过 ListActionType
枚举类型来定义的;这些操作的实现类在 com.haulmont.cuba.gui.components.actions
包。
在表格中使用标准行为的示例:
<table id="usersTable" width="100%">
<actions>
<action id="create"/>
<action id="edit"/>
<action id="remove"/>
<action id="refresh"/>
</actions>
<buttonsPanel>
<button action="usersTable.create"/>
<button action="usersTable.edit"/>
<button action="usersTable.remove"/>
<button action="usersTable.refresh"/>
</buttonsPanel>
<rowsCount/>
<columns>
<column id="login"/>
...
</columns>
<rows datasource="usersDs"/>
</table>
下面详细介绍这些行为:
CreateAction - 创建
CreateAction
– 使用 create 标识符的 action。用来创建新的实例并且打开编辑界面。如果在编辑界面成功的提交了一个新的实体实例到数据库,CreateAction
会将这个新的实例添加到表格的数据源,并且在界面上使这个实体成为选中状态。
CreateAction
类中定义了下面这些特殊的方法:
-
setOpenType()
可以设置新实体编辑界面的打开模式。默认THIS_TAB - 当前标签页
。因为通过其它模式打开编辑界面的需求是很常见的(比如,
DIALOG - 对话框
模式),可以在使用声明式方式创建create
行为的时候,在action
元素的openType
属性指定需要的打开模式。通过这种方式可以避免在界面控制器获取 action 引用通过编程的方式设置。示例:<table id="usersTable"> <actions> <action id="create" openType="DIALOG"/>
-
setWindowId()
可以设置实体编辑界面的标识符。默认情况下,使用{entity_name}.edit
,比如sales$Customer.edit
。 -
setWindowParams()
可以设置传递给编辑界面的init()
方法的参数。这些参数可以通过@WindowParam
注解注入到界面控制器中,或者也可以在数据源查询中通过param$
前缀直接使用。 -
setWindowParamsSupplier()
跟setWindowParams()
的不同之处在于,这个方法可以在 action 即将要被调用的时候修改编辑窗口的参数值。可以提供新的参数,新的参数会跟setWindowParams()
方法中提供的参数合并,并且覆盖之前的参数。示例:createAction.setWindowParamsSupplier(() -> { Customer customer = metadata.create(Customer.class); customer.setCategory(/* some value dependent on the current state of the screen */); return ParamsMap.of("customer", customer); });
-
setInitialValues()
可以设置将要编辑的实体的属性初始化值。这个方法接收一个Map
对象,键值是属性名称,值为属性值。示例:Map<String, Object> values = new HashMap<>(); values.put("type", CarType.PASSENGER); carCreateAction.setInitialValues(values);
在 使用 CreateAction 进行初始化 章节也提供一个使用
setInitialValues()
的例子。 -
setInitialValuesSupplier()
跟setInitialValues()
的不同之处在于,这个方法可以在 action 即将要被调用的时候修改实体初始化的值。可以提供新的参数,新的参数会跟setInitialValues()
方法中提供的参数合并,并且覆盖之前的参数。示例:carCreateAction.setInitialValuesSupplier(() -> ParamsMap.of("type", /* value depends on the current state of the screen */));
-
setBeforeActionPerformedHandler()
可以提供一个处理函数,这个函数在 action 执行之前调用。这个函数返回值是true
的话,action 会继续执行;返回false
终止执行。示例:customersTableCreate.setBeforeActionPerformedHandler(() -> { showNotification("The new customer instance will be created"); return isValid(); });
-
afterCommit()
在新实体成功提交到数据库并且编辑界面关闭之后会调用此方法。这个方法本身没有实现,可以通过继承重写此方法来处理这个事件。 -
setAfterCommitHandler()
提供一个处理函数,在新实体成功提交到数据库并且编辑界面关闭之后会调用此函数。可以通过提供此函数避免创建 action 的子类并重写afterCommit()
方法。示例:@Named("customersTable.create") private CreateAction customersTableCreate; @Override public void init(Map<String, Object> params) { customersTableCreate.setAfterCommitHandler(new CreateAction.AfterCommitHandler() { @Override public void handle(Entity entity) { showNotification("Committed", NotificationType.HUMANIZED); } }); }
-
afterWindowClosed()
不管实体是否提交,只要关闭了编辑界面就会最后调用此方法。这个方法本身没有实现,可以通过继承重写此方法来处理这个事件。 -
setAfterWindowClosedHandler()
提供一个处理函数,不管实体是否提交,只要关闭了编辑界面就会最后调用此函数。可以通过提供此函数避免创建 action 的子类并重写afterWindowClosed()
方法。
EditAction - 编辑
EditAction
是使用 edit 标识符的 action,用来为选中的实体实例打开编辑界面。如果编辑界面成功的将实例保存到数据库,EditAction
会更新表格数据源中的实例。
EditAction
类中定义了下面这些特殊的方法:
-
setOpenType()
可以设置实体编辑界面的打开模式。默认THIS_TAB - 当前标签页
。因为通过其它模式打开编辑界面的需求是很常见的(比如,
DIALOG - 对话框
模式),可以在使用声明式方式创建edit
行为的时候,在action
元素的openType
属性指定需要的打开模式。通过这种方式可以避免在界面控制器获取 action 引用通过编程的方式设置。示例:<table id="usersTable"> <actions> <action id="edit" openType="DIALOG"/>
-
setWindowId()
可以设置实体编辑界面的标识符。默认情况下,使用{entity_name}.edit
,比如sales$Customer.edit
。 -
setWindowParams()
可以设置传递给编辑界面的init()
方法的参数。这些参数可以通过@WindowParam
注解注入到界面控制器中,或者也可以在数据源查询中通过param$
前缀直接使用。 -
setWindowParamsSupplier()
跟setWindowParams()
的不同之处在于,这个方法可以在 action 即将要被调用的时候修改编辑窗口的参数值。可以提供新的参数,新的参数会跟setWindowParams()
方法中提供的参数合并,并且覆盖之前的参数。示例:customersTableEdit.setWindowParamsSupplier(() -> ParamsMap.of("category", /* some value dependent on the current state of the screen */));
-
setBeforeActionPerformedHandler()
可以提供一个处理函数,这个函数在 action 执行之前调用。这个函数返回值是true
的话,action 会继续执行;返回false
终止执行。示例:customersTableEdit.setBeforeActionPerformedHandler(() -> { showNotification("The customer instance will be edited"); return isValid(); });
-
afterCommit()
在新实体成功提交到数据库并且编辑界面关闭之后会调用此方法。这个方法本身没有实现,可以通过继承重写此方法来处理这个事件。 -
setAfterCommitHandler()
提供一个处理函数,在新实体成功提交到数据库并且编辑界面关闭之后会调用此函数。可以通过提供此函数避免创建 action 的子类并重写afterCommit()
方法。示例:@Named("customersTable.edit") private EditAction customersTableEdit; @Override public void init(Map<String, Object> params) { customersTableEdit.setAfterCommitHandler(new EditAction.AfterCommitHandler() { @Override public void handle(Entity entity) { showNotification("Committed", NotificationType.HUMANIZED); } }); }
-
afterWindowClosed()
不管实体是否提交,只要关闭了编辑界面就会最后调用此方法。这个方法本身没有实现,可以通过继承重写此方法来处理这个事件。 -
setAfterWindowClosedHandler()
提供一个处理函数,不管实体是否提交,只要关闭了编辑界面就会最后调用此函数。可以通过提供此函数避免创建 action 的子类并重写afterWindowClosed()
方法。 -
getBulkEditorIntegration()
为表格批量编辑提供了可能性。表格需要设置multiselect
属性启用。当表格选中多行的时候,如果触发EditAction
行为,则会打开批量编辑器组件进行批量编辑。返回的
BulkEditorIntegration
实例可以通过下面的方法进行进一步处理:-
setOpenType()
, -
setExcludePropertiesRegex()
, -
setFieldValidators()
, -
setModelValidators()
, -
setAfterEditCloseHandler()
.
@Named("clientsTable.edit") private EditAction clientsTableEdit; @Override public void init(Map<String, Object> params) { super.init(params); clientsTableEdit.getBulkEditorIntegration() .setEnabled(true) .setOpenType(WindowManager.OpenType.DIALOG); }
-
RemoveAction - 删除
RemoveAction
- 是使用 remove 标识符的 action,用来删除选中的实体实例。
RemoveAction
类中定义了下面这些特殊的方法:
-
setAutocommit()
可以控制从数据库删除的动作是否提交。默认情况下,在动作触发之后会调用commit()
提交从数据库删除实体。可以通过setAutocommit()
方法或者设置构造器中对应的参数将autocommit
属性设置为 false 来禁用自动提交。这样的话,需要显式调用数据源的commit()
方法来提交改动。autocommit
的值不会影响Datasource.CommitMode.PARENT
模式下的数据源,比如,提供组合实体编辑的数据源。 -
setConfirmationMessage()
设置删除数据确认窗口的信息文本。 -
setConfirmationTitle()
设置删除确认窗口的标题。 -
setBeforeActionPerformedHandler()
可以提供一个处理函数,这个函数在 action 执行之前调用。这个函数返回值是true
的话,action 会继续执行;返回false
终止执行。示例:customersTableRemove.setBeforeActionPerformedHandler(() -> { showNotification("The customer instance will be removed"); return isValid(); });
-
afterRemove()
当实体被成功删除之后,调用此方法。这个方法本身没有实现,可以通过继承重写此方法来处理这个事件。 -
setAfterRemoveHandler()
提供一个处理函数,在实体成功从数据库删除之后会调用此函数。可以通过提供此函数避免创建 action 的子类并重写afterRemove()
方法。示例:@Named("customersTable.remove") private RemoveAction customersTableRemove; @Override public void init(Map<String, Object> params) { customersTableRemove.setAfterRemoveHandler(new RemoveAction.AfterRemoveHandler() { @Override public void handle(Set removedItems) { showNotification("Removed", NotificationType.HUMANIZED); } }); }
RefreshAction - 刷新
RefreshAction
- 是使用 refresh 标识符的 action。用来更新(重新加载)实体集合。当触发时,这个动作会调用相应组件绑定的数据源中的 refresh()
方法。
RefreshAction
类中定义了下面这些特殊的方法:
-
setRefreshParams()
可以设置传递给CollectionDatasource.refresh()
方法的参数,这些参数之后会用在数据源查询中。默认情况不会带任何参数。 -
setRefreshParamsSupplier()
跟setRefreshParams()
的不同之处在于,这个方法可以在 action 即将要被调用的时候修改编辑窗口的参数值。可以提供新的参数,新的参数会跟setRefreshParams()
方法中提供的参数合并,并且覆盖之前的参数。示例:customersTableRefresh.setRefreshParamsSupplier(() -> ParamsMap.of("number", /* some value dependent on the current state of the screen */));
AddAction - 添加
AddAction
– 是使用 add 标识符的 action,用来选择一个已存在的实体并添加到集合中。当触发时,会打开实体的查找界面。
AddAction
类中定义了下面这些特殊的方法:
-
setOpenType()
可以设置实体选择界面的打开模式。默认THIS_TAB - 当前标签页
。因为通过其它模式打开查找界面的需求是很常见的(比如,
DIALOG - 对话框
模式),可以在使用声明式方式创建add
行为的时候,在action
元素的openType
属性指定需要的打开模式。通过这种方式可以避免在界面控制器获取 action 引用通过编程的方式设置。示例:<table id="usersTable"> <actions> <action id="add" openType="DIALOG"/>
-
setWindowId()
可以设置实体查找界面的标识符。默认情况下,使用{entity_name}.lookup
,比如sales$Customer.lookup
。如果不存在这种类型的界面,则会尝试打开{entity_name}.browse
界面,比如sales$Customer.browse
。 -
setWindowParams()
可以设置传递给查找界面的init()
方法的参数。这些参数可以通过@WindowParam
注解注入到界面控制器中,或者也可以在数据源查询中通过param$
前缀直接使用。 -
setWindowParamsSupplier()
跟setWindowParams()
的不同之处在于,这个方法可以在 action 即将要被调用的时候修改编辑窗口的参数值。可以提供新的参数,新的参数会跟setWindowParams()
方法中提供的参数合并,并且覆盖之前的参数。示例:tableAdd.setWindowParamsSupplier(() -> ParamsMap.of("customer", getItem()));
-
setHandler()
可以设置一个实现了Window.Lookup.Handler
接口的对象,这个对象会传递给查找界面。默认情况下,会使用AddAction.DefaultHandler
对象。 -
setBeforeActionPerformedHandler()
可以提供一个处理函数,这个函数在 action 执行之前调用。这个函数返回值是true
的话,action 会继续执行;返回false
终止执行。示例:customersTableAdd.setBeforeActionPerformedHandler(() -> { notifications.create() .withCaption("The new customer will be added") .show(); return isValid(); });
ExcludeAction - 排除
ExcludeAction
- 是使用 exclude 标识符的 action。允许用户从一个实体集合中排除实体实例,而并不会从数据库删除。这个 action 的类是继承于 RemoveAction
,但是在触发这个动作的时候调用的是 CollectionDatasource
里的 excludeItem()
而不是 removeItem()
。此外,对于嵌套数据源中的实体,ExcludeAction
动作会将子实体跟父实体的连接断开。因此这个 action 可以用来编辑一对多的关联关系。
除了 RemoveAction
里面的方法外,ExcludeAction
类中定义了下面这些特殊的方法:
-
setConfirm()
– 定义是否要显示确认删除窗口。也可以通过 action 的构造器设置这个参数。默认值是false
。 -
setBeforeActionPerformedHandler()
可以提供一个处理函数,这个函数在 action 执行之前调用。这个函数返回值是true
的话,action 会继续执行;返回false
终止执行。示例:
customersTableExclude.setBeforeActionPerformedHandler(() -> { showNotification("The selected customer will be excluded"); return isValid(); });
ExcelAction - 导出 Excel
ExcelAction
- 是使用 excel 标识符的 action。用来将表格数据导出成 XLS 格式的文件,并且下载。只能在 Table,GroupTable 和 TreeTable 组件添加此行为。
当通过编程的方式创建这个行为的时候,可以用实现了 ExportDisplay
接口的类为文件下载设置 display
参数。默认情况下使用标准的实现类。
ExcelAction
类中定义了下面这些特殊的方法:
-
setFileName()
- 设置 Excel 文件名称,不包含文件名后缀。 -
getFileName()
- 返回 Excel 文件名称,不包含文件名后缀。 -
setBeforeActionPerformedHandler()
可以提供一个处理函数,这个函数在 action 执行之前调用。这个函数返回值是true
的话,action 会继续执行;返回false
终止执行。示例:customersTableExcel.setBeforeActionPerformedHandler(() -> { showNotification("The selected data will ve downloaded as an XLS file"); return isValid(); });
3.6.5. 选取器控件的标准行为(历史版本)
对于 PickerField,LookupPickerField 和 SearchPickerField 组件,一组标准行为是通过 PickerField.ActionType
枚举类型来定义的;操作的实现是 PickerField
接口的内部类,下面详细介绍这些实现。
在选取器组件中使用标准操作的示例:
<searchPickerField optionsDatasource="coloursDs"
datasource="carDs" property="colour">
<actions>
<action id="clear"/>
<action id="lookup"/>
<action id="open"/>
</actions>
</searchPickerField>
LookupAction - 查找
LookupAction
– 使用 lookup 标识符的 action。用来选取实体实例然后将选中的实体设置成组件的值。当触发这个动作的时候,会打开实体的查找界面。
LookupAction
类中定义了下面这些特殊的方法:
-
setLookupScreenOpenType()
可以设置实体选择界面的打开模式。默认THIS_TAB - 当前标签页
。 -
setLookupScreen()
可以设置实体查找界面的标识符。默认情况下,使用{entity_name}.lookup
,比如sales$Customer.lookup
。如果不存在这种类型的界面,则会尝试打开{entity_name}.browse
界面,比如sales$Customer.browse
。 -
setLookupScreenParams()
可以设置传递给查找界面的init()
方法的参数。 -
afterSelect()
当选择的实体被设置到可视化组件的值之后,调用此方法。这个方法本身没有实现,可以通过继承重写此方法来处理这个事件。 -
afterCloseLookup()
不管是否选择了实体,只要关闭了查找界面就会最后调用此方法。这个方法本身没有实现,可以通过继承重写此方法来处理这个事件。
ClearAction - 清空
ClearAction
- 使用 clear 标识符的 action。用来清空组件的值(比如设置成 null
)。
OpenAction - 打开
OpenAction
- 使用 open 标识符的 action。用来打开编辑组件当前值关联实体的编辑界面。
OpenAction
类中定义了下面这些特殊的方法:
-
setEditScreenOpenType()
可以设置实体编辑界面的打开模式。默认THIS_TAB - 当前标签页
。 -
setEditScreen()
可以设置实体编辑界面的标识符。默认情况下,使用{entity_name}.edit
,比如sales$Customer.edit
。 -
setEditScreenParams()
可以设置传递给编辑界面的init()
方法的参数。 -
afterWindowClosed()
关闭了编辑界面之后调用此方法。这个方法本身没有实现,可以通过继承重写此方法来处理这个事件。
3.6.6. screens.xml (历史版本)
使用从 v.7.0 新 API 写的界面不需要注册。而是通过 |
这种类型的文件是在 Web 客户端的通用用户界面中用来注册界面的 XML 描述。
文件路径通过 cuba.windowConfig 应用程序属性指定。当在 Studio 创建新项目时,会在 web 模块包的根目录创建 web-screens.xml
文件,比如 modules/web/src/com/company/sample/web-screens.xml
。
这个文件有如下结构:
screen-config
– 根节点元素。包含如下元素:
-
screen
– 界面描述元素。screen
属性:-
id
– 界面标识符,可以使用程序代码的标识符(比如在Frame.openWindow()
和其它方法中),也可以是用 menu.xml 中的标识符。 -
class
– 如果没设置template
属性,使用此属性指定实现了Callable
或者Runnable
接口的类。如果是实现了
Callable
接口,call()
方法应当返回一个Window
的实例,这个实例会返回给调用段代码,作为WindowManager.openWindow()
的结果。这个类可以包含带有字符串参数的构造器,通过嵌套的param
元素定义(参考下面)。 -
agent
- 如果使用同一个id
注册了多个模板(templates),使用这个属性来选择打开哪个模板。有三种标准的代理(agent)类型:DESKTOP
桌面,TABLET
平板,PHONE
手机。这些类型支持按照当前设备和显示参数选择界面模板。参考界面代理了解细节。 -
multipleOpen
– 可选属性,允许设置界面可以多次打开。如果没设置或者设置成false
,并且由此标识符定义的界面已经在主窗口打开了,系统会显示已经存在的界面,而不会打开一个新的。如果设置成true
,则可以打开任意数量的界面。
screen
的元素:-
param
– 以 map 的形式定义界面参数传入控制器的init()
方法。而通过调用openWindow()
方法传递的参数,会覆盖screens.xml
中定义的相应名称的参数。param
属性:-
name
– 参数名称 -
value
– 参数值。字符串类型,如果是true
或者false
,则会被转换成相应的Boolean
值。
-
-
-
include
– 包含一个不通的文件,比如screens.xml
。include
属性:-
file
– 按照 Resources 接口规则定义的文件路径。
-
screens.xml
文件示例:
<screen-config xmlns="http://schemas.haulmont.com/cuba/screens.xsd">
<screen id="sales$Customer.lookup" template="/com/sample/sales/gui/customer/customer-browse.xml"/>
<screen id="sales$Customer.edit" template="/com/sample/sales/gui/customer/customer-edit.xml"/>
<screen id="sales$Order.lookup" template="/com/sample/sales/gui/order/order-browse.xml"/>
<screen id="sales$Order.edit" template="/com/sample/sales/gui/order/order-edit.xml"/>
</screen-config>
3.7. 前端用户界面
前端界面客户端block提供了使用移动端优先的响应式 webUI 快速创建面向客户的门户的能力。
使用基于 Node.js 的 包 来创建前端客户端及其界面。
3.7.1. 在 Studio 中添加前端 UI
如需在项目中添加前端客户端模块:
-
在 CUBA Studio 中打开 CUBA 项目树;
-
右键点击 Modules 节点;
-
选择 Manage modules > Create 'front' module 条目。
在创建模块完成之后,启动应用程序,并在浏览器打开 http://localhost:8080/app-front
。会展示一个登录表单。登录之后,会显示带有垂直菜单栏以及响应式布局的主窗口。
3.7.2. Polymer 用户界面
基于 Polymer 库,提供跟手机端浏览器紧密集成的功能,可以添加 web 应用程序到设备主页面或者离线工作。
CUBA 框架 Polymer UI 有如下功能:
平台目前的策略还是关注用来创建 Progressive Web Apps 的 Polymer 技术和工具。目标是为了按照 Polymer 的规范和最佳实践,提供 CUBA 用户类似的学习曲线和开发体验。Polymer 应用具有基于组件的架构,由 Web 组件 构成。
要快速开始开发,需要熟悉 Polymer 的基本概念。这里有个关于 Polymer 的非常快速的介绍: https://github.com/Polymer/polymer#polymer-in-1-minute 。但是,最好还是进行深入了解: ttps://polymer-library.polymer-project.org/2.0/start/ 。因为 Polymer 是按照大多数情况下学习 web 平台本身的标准构建的。
3.7.2.3. 构建系统和项目结构
Polymer 客户端构建链会使用以下工具:
默认 Gradle 会处理这些工具的安装和调用。但是也可以直接使用这些工具,参考使用原生 Polymer 工具。
Polymer 2.x 和相应的原生元素都是用 ES6 标准编写的,因此需要一个额外的编译步骤(ES6 → ES5 ttps://polymer-library.polymer-project.org/2.0/docs/es6[编译])来适应老的浏览器。
Polymer 客户端的默认 preset 是 |
修改构建 preset,打开 polymer.json
文件,修改 builds
属性内容,示例:
"builds": [
{
"preset": "es5-bundled",
"basePath": "/app-front/",
"addServiceWorker": false
}
]
可以在 polymer.json
中设置多个 preset 或者自定义构建流程。参考 Polymer 网站 了解更多关于 preset 的信息以及编译选项。
如果需要将特殊的 preset 编译结果部署到 Tomcat,需要修改 build.gradle
内的 deploy
任务:
task deploy(type: Copy, dependsOn: [assemble, deployUnbundled]) {
from file('build/es5-unbundled')
into "$cuba.tomcat.dir/webapps/$frontAppDir"
}
注意,es6-bundled
→ es5-unbundled
需要在 polymer.json
和 build.gradle
两处进行修改。
3.7.2.3.1. 目录结构
front/ |-- src/ | |-- app-shell.html | |-- shared-styles.html |-- images | |-- app-icon/ | |-- favicon.ico |-- .gitignore |-- bower.json |-- index.html |-- manifest.json |-- package.json |-- polymer.json |-- service-worker.js |-- sw-precache-config.js
- src
-
放置前端组件的目录。
- package.json
-
Node.js 的依赖列表,用在构建阶段。
- bower.json
-
web 类库依赖列表(主要是 web 组件),运行时使用。
- polymer.json
-
Polymer 构建配置.
- index.html
-
应用程序入口。包含加载 polyfills 的逻辑以及导入 <appname>-shell.html。
- manifest.json
-
Web app 的装箱清单。包含当应用被添加到设备主界面时所需的信息。 参考了解更多: https://developer.mozilla.org/en-US/docs/Web/Manifest
- service-worker.js
-
Service worker 脚手架代码。
- sw-precache-config.js
-
sw-precache 类库使用的配置文件,用来在构建时(默认关闭)生成 Service worker。参考 离线能力。
3.7.2.3.2. 热部署
当使用 CUBA Studio 或者 Gradle 运行或者部署应用程序时,构建系统会按照 polymer.json
文件的配置对界面组件进行打包。默认情况下,整个应用程序会被打包成一个 app-shell.html
文件。当对某些应用组件进行更改的时候,Studio 会将改动热部署至 Tomcat。还有,会使用未打包的版本来替换打包的 app-shell.html
文件,以便更改能生效,如果直接在 tomcat/webapps
部署生产环境的话,需要注意这一点。
如果使用 |
如果使用基于 TypeScript 的客户端,需要手动运行 |
3.7.2.3.3. 使用原生 Polymer 工具
在开发 Polymer 组件的时候可以使用原生的 Polymer 框架工具集。这样的话如果项目里有一个单独的团队做前端就会很方便,系统需要安装 Node.js
。
全局安装 bower
和 polymer-cli
:
npm install bower polymer-cli -g
然后可以不用 Gradle 来构建和运行 web 应用程序:
cd modules/front
npm install
bower install
polymer serve
如果想要使用 Polymer 服务来做 app 的后台服务(而不是 Tomcat),需要在 modules/front/index.html
里面指定 REST API 的绝对路径,示例:
<myapp-shell api-url="http://localhost:8080/app/rest/"></myapp-shell>
然后可以通过 http://localhost:8081
(也许需要根据命令行输出更改端口号)访问 web 应用,这个 web 应用对应的后台 REST API 在 http://localhost:8080/app/rest/
。
3.7.2.4. CUBA Polymer Web 组件
CUBA 元素的详细 API 参考可以在 这里 找到。
3.7.2.4.1. 初始化
为了使用任何 cuba-
元素,需要用 cuba-app
元素来初始化通用类库以及 REST API 连接:
<cuba-app api-url="/app/rest/"></cuba-app>
这个只需要在 app 中放在一个地方(一次),而且尽可能在程序初始化的早期。不要在初始化之后动态改变属性值,也不要挂载/脱挂(attach/detach)其它元素。
3.7.2.4.2. 处理数据
如果需要加载数据,只需要在 HTML 中放置需要的 cuba-data 元素,并且设置必须属性。
实体加载
使用 cuba-entities 来加载实体。一旦设置了 entity-name
和 view
属性,这个元素可以加载实体列表,并且通过 data
属性将数据暴露给 Polymer 数据绑定的变量:
<cuba-entities entity-name="sec$User" view="_local" data="{{users}}"></cuba-entities>
然后可以很简单的展示数据:
<template is="dom-repeat" items="[[users]]" as="user">
<div>[[user.login]]</div>
</template>
实体查询
按照 此处描述定义一个查询。
使用 cuba-query 元素来获取查询结果。可以选择性的通过 params
属性传递参数:
<cuba-query id="query"
auto="[[auto]]"
entity-name="sec$User"
query-name="usersByName"
data="{{users}}">
</cuba-query>
<template is="dom-repeat" items="[[users]]" as="user">
<div>[[user.login]]</div>
</template>
服务调用
用 此处的方法暴露服务及其方法。使用 cuba-service 元素来调用这个方法:
<cuba-service service-name="cuba_ServerInfoService"
method="getReleaseNumber"
data="{{releaseNumber}}"
handle-as="text"></cuba-service>
Release number: [[releaseNumber]]
实体创建
cuba-entity-form
和 cuba-service-form
元素能帮助发送数据到后台。
在下面的例子中,绑定 user
对象到 entity
属性,并通过这个属性保存到数据库。
<cuba-entity-form id="entityForm"
entity-name="sec$User"
entity="[[user]]"
on-cuba-form-response="_handleFormResponse"
on-cuba-form-error="_handleFormError">
<label>Login: <input type="text" name="login" value="{{user.login::input}}"></label>
<label>Name: <input type="text" name="login" value="{{user.name::input}}"></label>
<button on-tap="_submit">Submit</button>
</cuba-entity-form>
<paper-toast id="successToast">Entity created</paper-toast>
<paper-toast id="errorToast">Entity creation error</paper-toast>
_submit: function() {
this.$.entityForm.submit();
},
_handleFormResponse: function() {
this.user = getUserStub();
this.$.successToast.open();
},
_handleFormError: function() {
this.$.errorToast.open();
}
如果在使用上面的例子中的 REST API 时不想强制用户登录,需要启用 REST API 匿名访问。 |
3.7.2.5. 样式
参考 Polymer 的 样式调整向导。跟传统的调样式方式最大的不同点就是怎样设置全局样式。因为 Polymer 元素使用的 Shadow DOM 全局样式,样式不会在组件内渗透。因此需要使用 style-modules。在 Polymer 客户端有个 shares-styles.html
文件,会被自动引用到 Studio 创建的新组件中。
3.7.2.6. 离线能力
试验功能! 下面列出的技术目前并不被所有浏览器支持(比如 service worker 在 Safari 还没实现) |
目前,跟 Polymer 一起,还提供 Progressive Web Applications 技术,比如 web app manifest 2 在用户的主页面展示 native-like - 接近原生 的样式。参考 Polymer 客户端模块的 manifest.json
文件。
主要有两种方式:
-
Service Worker 主要用来缓存应用本身。可以看看跟 Polymer 客户端一起生成的
sw-precache-config.js
文件。如果要启用自动生成 Service Worker,在polymer.json
中设置"addServiceWorker": true
。
更多关于配置和使用 Service Worker 的内容可以参考 这里 。
-
Local storage 和 Indexed DB 用来在本地保存数据。这些功能在相应的 Polymer 元素中提供: app-localstorage-document app-indexeddb-mirror 。
3.7.2.7. TypeScript 支持
从 CUBA Release 6.9 开始,可以通过 Studio 搭建基于 TypeScript 的 Polymer 客户端脚手架代码。当创建 Polymer 客户端模块的时候,可以选择客户端的 polymer2-typescript
preset。跟基本 JavaScript 版本的主要不同是:
-
组件类(Component classes)放在分开单独的
*.ts
文件
namespace myapp {
const {customElement} = Polymer.decorators;
@customElement('myapp-component')
class MyappComponent extends Polymer.Element {
}
}
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="./shared-styles.html">
<dom-module id="myapp-component">
<template>
<!-- some html markup -->
</template>
<script src="myapp-component.js"></script>
</dom-module>
- 构建过程额外增加了一个阶段 - TypeScript 编译
-
参考
package.json
的scripts
部分:
{
"scripts": {
"build": "npm run compile && polymer build",
"compile": "tsc",
"watch": "tsc -w"
}
}
现在,在 polymer build
命令之前,需要先运行 npm run compile
命令,这个命令会有效的执行 TypeScript 编译(tsc
)。
如果修改了组件类的代码,并且希望通过 Studio 的热部署使改动生效,需要先手动在 |
3.7.2.7.1. 使用 TypeScript 创建 Polymer 组件
Polymer 团队通过 TypeScript 装饰器提供了更方便和简洁的创建组件类的途径。先看看下面这个例子:
/// <reference path="../bower_components/cuba-app/cuba-app.d.ts" />
/// <reference path="../bower_components/app-layout/app-drawer/app-drawer.d.ts" />
/// <reference path="../bower_components/app-layout/app-drawer-layout/app-drawer-layout.d.ts" />
namespace myapp {
// Create shortcuts to decorators
const {customElement, property, observe, query} = Polymer.decorators;
@customElement('myapp-component')
class MyappComponent extends (Polymer.mixinBehaviors([CubaAppAwareBehavior, CubaLocalizeBehavior], Polymer.Element) as
new () => Polymer.Element & CubaAppAwareBehavior & CubaLocalizeBehavior) {
@property({type: Boolean})
enabled: boolean;
@property({type: String})
caption: string;
@query('#drawer')
drawer: AppDrawerElement;
@observe('app')
_init(app: cuba.CubaApp) {
...
}
@computed('enabled', 'caption')
get enabledCaption() {
...
}
}
}
-
/// <reference path="…"/>
- 允许通过这种方式引入 TypeScript 中其它元素或者类库。 -
@customElements('element-name')
装饰器使得无需定义static get is()
方法,并且无需手动调用customElements.define()
。 -
@property()
装饰器可以指定组件的属性。 -
@query('.css-selector')
装饰器可以给组件选择 DOM 元素。 -
@observe('propertyName')
装饰器可以定义属性观察者。 -
@computed()
装饰器可以定义计算方法。
参考 polymer-decorators github 仓库了解更多示例。
3.7.2.8. 问题分析
- Proxy - 代理
-
如果在一个有代理服务器的环境工作,需要配置按照要求配置 bower 和 npm。为了使得 bower 和 npm 能在代理服务器环境工作,需要在
modules/front/
目录创建以下文件:.bowerrc
{ "proxy":"http://<user>:<password>@<host>:<port>", "https-proxy":"http://<user>:<password>@<host>:<port>" }
.npmrc
proxy=http://<user>:<password>@<host>:<port> https-proxy=http://<user>:<password>@<host>:<port>
- NPM install 失败
-
Windows 环境的
npm install
有个 已知问题。
有可能在构建过程遇到下列错误:
npm ERR! code EPERM npm ERR! errno -4048 npm ERR! syscall rename npm ERR! Error: EPERM: operation not permitted,
作为暂时方案(workaround),可以禁用 Windows 防火墙(Defender)或者其它的反病毒软件,确保没在任何 IDE 打开项目,然后再次运行构建过程。
可以关注这个 问题 了解将来可能的解决方案。
3.7.3. 基于 React 的用户界面
React UI 的文档放在 front
模块的 README.md
中。最新版可以在 GitHub 找到。
3.8. Portal 组件
在本手册中,portal 是一个客户端 block,这个客户端主要用来:
-
提供另一个可选的 web 页面,一般是为组织外部的用户使用;
-
提供集成手机应用和第三方系统的接口。
一个具体的应用程序可能因为不同的目的而设计几个不同的 portal 模块;比如,在一个出租车业务自动化的应用中,可能有一个对外部用户的公共 web 网站,还有一个手机应用中集成的模块用来预约出租车,以及一个给司机用的手机应用集成的界面,等等。
cuba 应用程序组件包含的 portal 模块,是一个用来在项目中创建 portal 的模板。提供了客户端 block 跟 Middleware 交互的基本功能。另外,全局 REST API 作为依赖被包含在 portal 模块中,并且默认开启。
下面是平台 portal 模块提供的主要组件的简介。
-
PortalAppContextLoader
– 用来加载 AppContext;必须在web.xml
文件的listener
元素中注册。 -
PortalDispatcherServlet
– 分发请求到 Spring MVC 控制器的主要 servlet,包括分发 web 页面请求和 REST API 请求。Spring 上下文配置文件通过 cuba.dispatcherSpringContextConfig 应用程序属性定义。servlet 必须在web.xml
注册并且映射到 web 应用的 URL 根节点。 -
App
– 包含当前 HTTP 请求信息和Connection
对象引用的对象。App
实例可以通过在应用程序代码中调用App.getInstance()
静态方法来取得。 -
Connection
– 允许用户登入/登出 Middleware。 -
PortalSession
– portal 特定的用户会话对象。通过 UserSessionSource 基础接口以及PortalSessionProvider.getUserSession()
静态方法返回。它有一个额外的
isAuthenticated()
方法,如果会话属于一个非匿名用户,则会返回true
。比如,用户使用用户名密码登录的时候。当用户第一次访问 portal 的时候,
SecurityContextHandlerInterceptor
会为此用户先创建一个匿名会话(或者绑定到已经存在的匿名会话),这个匿名会话是通过使用指定在 cuba.portal.anonymousUserLogin 参数的匿名用户名在 Middleware 注册得到的。这个注册是通过 loginTrusted() 方法完成,所以也需要在 portal 设置 cuba.trustedClientPassword 参数。因此 portal 的任何匿名用户可以使用cuba.portal.anonymousUserLogin
用户权限跟 Middleware 交互。如果 portal 包含使用用户名和密码的用户注册界面,
SecurityContextHandlerInterceptor
会在Connection.login()
执行后,将这个注册用户的会话传递给执行业务逻辑的线程,从而使得接下来跟 Middleware 的交互是以此用户的名义发出的。 -
PortalLogoutHandler
– 负责导航到登出页面。需要在portal-security-spring.xml
项目文件中注册。
3.9. REST API
全局的 REST API 提供以下功能:
-
实体的增删查改(CRUD)操作。
-
执行预定义的 JPQL 查询语句。
-
执行服务(Service)方法。
-
获取元数据(实体、视图、枚举、数据类型)。
-
获取当前用户权限(访问实体、实体属性、特殊权限)。
-
获取当前用户信息(名称、语言、时区等)。
-
上传和下载文件。
REST API 使用 OAuth2 协议来做认证,并且支持匿名访问。
API 端点(endpoint)的详细文档: http://files.cuba-platform.com/swagger/7.0 。
使用 REST API cookbook 章节包含了很多实际使用 REST API 的例子。以下提供一些功能和配置选项的正式说明。
3.9.1. 预定义 JPQL 查询配置
在 CUBA 应用程序中,预定义的 JPQL 查询语句需要写在特定的配置文件中,这些配置文件是由 web 或者 portal 模块的 cuba.rest.queriesConfig 应用程序属性来指定,比如在 web-app.properties
文件中:
cuba.rest.queriesConfig = +com/company/myapp/rest-queries.xml
rest-queries.xml
这个文件必须放在 web 或者 portal 模块的根目录(比如 com.company.myapp
)。文件内容格式是由 rest-queries.xsd shema 定义。示例:
<?xml version="1.0"?>
<queries xmlns="http://schemas.haulmont.com/cuba/rest-queries.xsd">
<query name="carByVin" entity="sample$Car" view="carEdit">
<jpql><![CDATA[select c from sample$Car c where c.vin = :vin]]></jpql>
<params>
<param name="vin" type="java.lang.String"/>
</params>
</query>
<query name="allColours" entity="sample$Colour" view="_local">
<jpql><![CDATA[select u from sample$Colour u order by u.name]]></jpql>
</query>
<query name="carsByIds" entity="sample$Car" view="carEdit" cacheable="true">
<jpql><![CDATA[select c from sample$Car c where c.id in :ids]]></jpql>
<params>
<param name="ids" type="java.util.UUID[]"/>
</params>
</query>
<query name="myOrders" entity="sample$Order" view="orderBrowse">
<jpql><![CDATA[select o from sample$Order o where o.createdBy = :session$userLogin]]></jpql>
</query>
</queries>
关于配置和执行查询语句的示例,可以参考 执行 JPQL 查询(GET) 和 执行 JPQL 查询(POST) 章节。
平台还提供了预定义的 all
查询用来获取指定实体类型的所有实例。这个还能和 /count
一起使用,用来获得实体的总数,示例:
http://localhost:8080/app/rest/v2/queries/sales$Order/all/count
XML 里面的 query
元素还可以使用 cacheable
属性来启用查询语句的缓存。
查询语句可以包含预定义的参数,这些参数使用当前用户的 id 和登录名:session$userId
和 session$userLogin
。不需要在 params
元素声明这两个参数(参考上面的例子)。
3.9.2. 服务配置
CUBA 应用中,service 的方法可以通过 REST API 访问,这些方法的列表需要配置在 xml 配置文件中,这个 xml 配置文件需要通过 web 或者 portal 模块的 cuba.rest.servicesConfig 应用程序属性来指定,比如在 web-app.properties
文件中:
cuba.rest.servicesConfig = +com/company/myapp/rest-services.xml
rest-services.xml
文件需要放在 web 或者 portal 模块的根目录(比如 com.company.myapp
)。文件内容格式是由 rest-services-v2.xsd shema 来定义,示例:
<?xml version="1.0" encoding="UTF-8"?>
<services xmlns="http://schemas.haulmont.com/cuba/rest-services-v2.xsd">
<service name="myapp_SomeService">
<method name="sum">
<param name="number1"/>
<param name="number2"/>
</method>
<method name="emptyMethod"/>
<method name="overloadedMethod">
<param name="intParam" type="int"/>
</method>
<method name="overloadedMethod">
<param name="stringParam" type="java.lang.String"/>
</method>
</service>
</services>
如果服务没有重载带有相同数量参数的方法的话,方法参数的类型可以省略。否则,参数类型必须要显式定义。
关于配置和调用服务的示例可以参考 调用服务方法(GET) 章节。
如果某些服务的方法需要在 匿名访问被禁用的情况下进行无认证访问,那么可以在服务配置文件中给这个方法添加 anonymousAllowed="true"
属性:
<?xml version="1.0" encoding="UTF-8"?>
<services xmlns="http://schemas.haulmont.com/cuba/rest-services-v2.xsd">
<service name="myapp_SomeService">
<method name="sum" anonymousAllowed="true">
<param name="number1"/>
<param name="number2"/>
</method>
</service>
</services>
3.9.3. 数据模型版本
REST API 可以处理数据模型更改。这个功能在这个场景很有用,比如,有些实体属性被重命名了,但是 REST API 客户端不知道这个改动,并且还是希望这个属性保留原来的名字。
这种情况下,REST API 允许定义实体 JSON 的转换规则。如果客户端应用在发送请求时,在查询参数里带上数据模型的版本,REST API 的 response 或者 request body 中的 JSON 会按照请求中特定的领域模型版本的转换规则进行转换。
JSON 的转换规则必须写配置文件中,这些配置文件要通过 web 或者 portal 模块的 cuba.rest.jsonTransformationConfig 应用程序属性来定义,比如 web-app.properties
文件:
cuba.rest.jsonTransformationConfig = +com/company/myapp/rest-json-transformations.xml
rest-json-transformations.xml
文件需要放在 web 或者 portal 模块(比如 com.company.myapp
)。这个文件的内容是遵循 rest-json-transformations.xsd schema 的。文件示例:
<?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">
<renameAttribute oldName="oldNumber" currentName="number"/>
<renameAttribute oldName="date" currentName="deliveryDate"/>
<toVersion>
<removeAttribute name="discount"/>
</toVersion>
</transformation>
<transformation modelVersion="1.0" currentEntityName="sales$Contractor">
<renameAttribute oldName="summary" currentName="total"/>
<renameAttribute oldName="familyName" currentName="lastName"/>
<fromVersion>
<removeAttribute name="city"/>
<removeAttribute name="country"/>
</fromVersion>
<toVersion>
<removeAttribute name="phone"/>
</toVersion>
</transformation>
<transformation modelVersion="1.1" currentEntityName="sales$NewOrder">
<renameAttribute oldName="date" currentName="deliveryDate"/>
</transformation>
</transformations>
在配置文件中定义的标准转换器可以进行下列实体 JSON 转换任务:
-
重命名实体
-
重命名实体属性
-
删除实体属性
JSON 转换在下列 REST API 端点(endpoint)生效:
-
/entities - 获取实体列表、获取单个实体、实体创建、实体更新、实体删除
-
/queries - 查询返回的实体 JSON 会被转换
-
/services - JSON 转换会发生在两个地方,service 方法返回的实体,以及作为参数传入 service 方法的实体。
如果对 REST API 的请求使用了 modelVersion
URL 参数指定数据模型版本,那么 JSON 转换就会生效。
参考数据模型版本化示例了解如何配置数据模型版本以及怎样在客户端程序使用。
3.9.4. 跨域请求(CORS)设置
默认情况下,CUBA 允许所有的 REST API 的跨域请求。限制请求来源地址列表,可以通过设置 cuba.rest.allowedOrigins 应用程序属性。
3.9.5. 匿名访问
默认情况下,匿名访问时被禁用的。通过应用程序属性 cuba.rest.anonymousEnabled 来启用。如果请求中没有包含 Authentication
信息头,这个请求会被认为是匿名的。匿名访问的情况下,SecurityContext 会包含匿名用户会话。
给匿名用户配置权限,必须给 cuba.anonymousLogin 参数定义的匿名用户指定 角色。
3.9.6. 其它 REST API 设置
cuba.rest.client.id - 定义默认 REST API 客户端 id。这个客户端不是用户,而是使用 API 的应用程序。
cuba.rest.client.secret - 定义默认 REST API 客户端密码。这个客户端不是用户,而是使用 API 的应用程序。
cuba.rest.client.tokenExpirationTimeSec - 定义默认客户端访问令牌(access token)的过期时间,单位为秒。
cuba.rest.client.refreshTokenExpirationTimeSec - 定义默认客户端刷新令牌(refresh token)的过期时间,单位为秒。
cuba.rest.client.authorizedGrantTypes - 默认客户端认证授权类型的列表。如果要禁用刷新令牌,从列表中删除 refresh_token
项。
cuba.rest.maxUploadSize - 定义通过 REST API 能上传的文件的最大体积。
cuba.rest.reuseRefreshToken - 定义是否可以重用刷新令牌。
cuba.rest.requiresSecurityToken - 定义是否需要在 JSON 中传递额外的系统属性。细节参考 集合属性的安全约束。
cuba.rest.tokenMaskingEnabled - 定义在系统日志中是否需要对 REST API 的令牌进行掩码处理。
3.9.7. 创建带 OAuth2 保护的自定义控制器
如果需要创建由 OAuth2 保护的自定义 REST 控制器,可以按照以下步骤:
-
假设有如下 REST 控制器:
package com.company.test.portal.myapi; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.company.test.services.SomeService; @RestController @RequestMapping("/myapi") public class MyController { @Inject protected SomeService someService; @GetMapping("/dosmth") public String doSmth() { return someService.getResult(); } }
-
在 web 或者 portal 模块包的根目录(
com.company.test
)创建一个新的 Spring 配置文件rest-dispatcher-spring.xml
。文件内容如下:<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:security="http://www.springframework.org/schema/security" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-4.2.xsd"> <!-- Define a base package for your controllers--> <context:component-scan base-package="com.company.test.portal.myapi"/> <security:http pattern="/rest/myapi/**" create-session="stateless" entry-point-ref="oauthAuthenticationEntryPoint" xmlns="http://www.springframework.org/schema/security"> <!-- Specify one or more protected URL patterns--> <intercept-url pattern="/rest/myapi/**" access="isAuthenticated()"/> <anonymous enabled="false"/> <csrf disabled="true"/> <cors configuration-source-ref="cuba_RestCorsSource"/> <custom-filter ref="resourceFilter" before="PRE_AUTH_FILTER"/> <custom-filter ref="cuba_AnonymousAuthenticationFilter" after="PRE_AUTH_FILTER"/> </security:http> </beans>
-
在模块的属性文件(比如
portal-app.properties
)里定义一个累加属性cuba.restSpringContextConfig
:cuba.restSpringContextConfig = +com/company/test/rest-dispatcher-spring.xml
-
这个新的控制器在
CubaRestApiServlet
上下文内运行。所以控制器内方法的 URL 以/rest
开头,比如 doSmth()方法可以通过 URLhttp://localhost:8080/app-portal/rest/myapi/dosmth
来访问。自定义控制器的 URL 绝对不能 以
/rest/v2
开头。
3.9.8. 集合属性的安全约束
考虑下面这个情况:
-
数据模型包含
Order
和OrderLine
实体,形成了一对多的关系。 -
REST 客户端获取了一个
Order
实例,并且带着嵌套的OrderLine
实例集合。 -
有些安全约束可以过滤掉某些
OrderLine
实例,从而使得客户端不加载这些实例,也不知道这些实例的存在。比方说,line5
不会被加载,但是存在于数据库。 -
如果客户端从集合中删除了一条,比如说
line2
,然后将整个组合通过/entities/{entityName}/{entityId}
API 端点进行保存,会有两种可能的输出:-
如果这个约束从实体被加载之后没有更改过,框架会首先在
OrderLine
集合里面恢复被过滤的line5
实例然后只删除line2
,这个是正确的行为。 -
但是如果这个约束在什么时候更改了,导致
line5
能被用户看见,此时,框架就不会正确的在集合内恢复这些被过滤的实例(因为从目前的约束状态看,line5
应该被客户加载过)。结果导致line2
和line5
都会被删除。
-
如果对上面描述的场景有担忧,可以通过这种方式排除数据丢失的隐患:在表示实体的 JSON 中发送一个特殊的系统属性。这个属性被称为 __securityToken
并且会自动包含在返回的 JSON 中,如果 cuba.rest.requiresSecurityToken 设置成 true
的话。REST 客户端的责任就是在保存实体的时候将这个属性带入并返回。
实体 JSON 包含安全令牌的示例:
{
"id": "fa430b56-ceb2-150f-6a85-12c691908bd1",
"number": "OR-000001",
"items": [
{
"id": "82e6e6d2-be97-c81c-c58d-5e2760ae095a",
"description": "Item 1"
},
{
"id": "988a8cb5-d61a-e493-c401-f717dd9a2d66",
"description": "Item 2"
}
],
"__securityToken": "0NXc6bQh+vZuXE4Fsk4mJX4QnhS3lOBfxzUniltchpxPfi1rZ5htEmekfV60sbEuWUykbDoY+rCxdhzORaYQNQ=="
}
__securityToken
属性包含了被过滤的实例标识符的加密串,从而使得框架总是能恢复需要的信息而不管约束怎么改变。
3.9.9. 令牌存储持久化
默认情况下,OAuth 令牌只是保存在内存。如果需要在数据库保存这些令牌,设置 cuba.rest.storeTokensInDb 应用程序属性为 true
。这个属性的值是保存在数据库的,因此可以在系统界面 Administration > Application Properties 修改这个值。
数据库中保存的过期令牌需要定期清理。这个定时任务的时间安排 cron 表达式通过 cuba.rest.deleteExpiredTokensCron 定义。
3.9.10. 项目特定的 Swagger 文档
REST API 通用接口文档可以通过 http://files.cuba-platform.com/swagger/7.0 获得。
任何运行的 CUBA 应用程序还有项目特定的接口文档,文档结构按照 Swagger specification version 2.0 生成。
文档可以通过下面这些 URL 获得:
-
/rest/v2/docs/swagger.yaml
- YAML 版本的通用接口文档 -
/rest/v2/docs/swagger.json
- JSON 版本的通用接口文档 -
/rest/v2/docs/swaggerDetailed.yaml
- YAML 版本的项目特定 Swagger 接口文档 -
/rest/v2/docs/swaggerDetailed.json
- JSON 版本的项目特定 Swagger 接口文档
示例:
http://localhost:8080/app/rest/v2/docs/swagger.yaml
http://localhost:8080/app/rest/v2/docs/swaggerDetailed.yaml
接口文档可以用来查看,测试或者生成 REST API 的客户端代码。参考以下工具: Swagger UI, Swagger Inspector, Postman, Swagger Codegen。
生成的文档包括:
3.9.11. 使用 REST API
本节包含 REST API 的用法示例。
相关 REST API 方法的详细信息是根据 Swagger 规范编写的,可在地址 http://files.cuba-platform.com/swagger/7.0 中找到。
3.9.11.1. 获取 OAuth 令牌
任何 REST API 方法都需要 OAuth 令牌(除了使用匿名访问)。可以通过 POST 方式请求此地址获取令牌:
http://localhost:8080/app/rest/v2/oauth/token
此端点使用基本身份验证进行访问保护,REST API 客户端 ID 和密码用于这里的份验证。请注意,这里的客户端 ID 和密码不是应用程序的用户登录名和密码。REST API 客户端 ID 和密码通过应用程序属性 cuba.rest.client.id 和 cuba.rest.client.secret 定义。(默认值分别为 client
和 secret
)。必须在 Authorization
请求头中以 base64 编码的字符串传递客户端 ID 和密码,客户端 ID 和密码使用冒号(":")分隔。
请求类型必须是 application/x-www-form-urlencoded
,编码方式为 UTF-8
。
请求必须包含以下参数:
-
grant_type
-password
。 -
username
- 应用程序用户登录名。 -
password
- 应用程序用户密码。
POST /oauth/token
Authorization: Basic Y2xpZW50OnNlY3JldA==
Content-Type: application/x-www-form-urlencoded
grant_type=password&username=smith&password=qwerty123
也可以使用 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
方法返回一个 JSON 对象:
{
"access_token": "29bc6b45-83cd-4050-8c7a-2a8a60adf251",
"token_type": "bearer",
"refresh_token": "e765446f-d49e-4634-a6d3-2d0583a0e7ea",
"expires_in": 43198,
"scope": "rest-api"
}
访问令牌值在 access_token
属性中。
要使用访问令牌,将其放在带有 Bearer
类型的 Authorization
请求头中,例如:
Authorization: Bearer 29bc6b45-83cd-4050-8c7a-2a8a60adf251
refresh_token
属性包含刷新令牌值。刷新令牌不能用于访问受保护的资源,但它具有比访问令牌更长的生命周期,并且可以用于在当前的访问令牌到期时获取新的访问令牌。
使用刷新令牌获取新访问令牌的请求必须包含以下参数:
-
grant_type
-refresh_token
. -
refresh_token
- 刷新令牌值.
POST /oauth/token
Authorization: Basic Y2xpZW50OnNlY3JldA==
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=e765446f-d49e-4634-a6d3-2d0583a0e7ea
另请参阅以下与令牌相关的应用程序属性:
3.9.11.2. 使用 LDAP 进行 REST API 身份验证
可以使用以下属性启用 REST 的 LDAP 身份验证:
-
cuba.rest.ldap.enabled
- 是否启用 LDAP 身份验证。 -
cuba.rest.ldap.urls
– LDAP 服务器 URL。 -
cuba.rest.ldap.base
– 用户搜索的基准 DN。 -
cuba.rest.ldap.user
– 有权从目录中读取信息的系统用户的专有名称。 -
cuba.rest.ldap.password
– cuba.web.ldap.user属性中定义的系统用户的密码。 -
cuba.rest.ldap.userLoginField
- 用于匹配登录名的 LDAP 用户属性的名称。默认情况下是sAMAccountName
(适用于 Active Directory)。
local.app.properties文件示例:
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
可以使用以下端点获取 OAuth 令牌:
http://localhost:8080/app/rest/v2/ldap/token
此端点使用基本身份验证进行访问保护。REST API 客户端标识符和密码用于基本身份验证。请注意,这些不是应用程序用户登录名和密码。REST API 客户端 ID 和密码在应用程序属性 cuba.rest.client.id 和 cuba.rest.client.secret 中定义。(默认值分别为 client
和 secret
)。必须在 Authorization
请求头中以 base64 编码的字符串传递客户端 ID 和密码,客户端 ID 和密码使用冒号(":")分隔。
请求参数与标准身份验证相同:
-
grant_type
- 总是password
. -
username
- 应用程序用户登录名。 -
password
- 应用程序用户密码。
请求类型必须是 application/x-www-form-urlencoded
,编码方式为 UTF-8
。
此外,可以禁用使用登录名和密码的标准身份验证:
cuba.rest.standardAuthenticationEnabled = false
3.9.11.3. 自定义验证
身份验证机制可以通过密钥、链接、LDAP 登录名和密码等提供访问令牌。REST API 使用特有的身份验证机制,无法被修改。要使用自定义身份验证过程,需要创建 REST 控制器并使用其 URL。
下面我们看看自定义身份验证机制,该机制可以通过推广码获取 OAuth 令牌。在下面的示例中,我们将使用包含带有 code
属性的 Coupon
实体的示例应用程序。我们将此属性的值作为 GET 请求中的身份验证参数发送。
-
创建一个带有
code
属性的Coupon
实体:@Column(name = "CODE", unique = true, length = 4) protected String code;
-
使用 promo-user 登录名创建一个用户,这个用户将会进行验证。
-
在 web 模块的根包(
com.company.demo
)下创建一个新的名为rest-dispatcher-spring.xml
的 Spring 配置文件。文件内容必须如下:<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>
-
将文件包含在
modules/web/src/web-app.properties
文件中的cuba.restSpringContextConfig
应用程序属性中:cuba.restSpringContextConfig = +com/company/demo/rest-dispatcher-spring.xml
-
在 web 模块的根包下创建
rest
包,并在其中实现自定义 Spring MVC 控制器。在自定义身份验证后,使用OAuthTokenIssuer
bean 为用户生成并发送 REST API 令牌:@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; } } }
-
在 web/core 模块的扫描中排除
rest
包:OAuthTokenIssuer
bean 仅在 REST API 上下文中可用,在应用程序上下文中扫描它会导致错误。<context:component-scan base-package="com.company.demo"> <context:exclude-filter type="regex" expression="com\.company\.demo\.web\.rest\..*"/> </context:component-scan>
-
现在,用户将能够使用带有
code
参数的 GET HTTP 请求获取 OAuth2 访问代码http://localhost:8080/app/rest/auth-code?code=A325
结果将是:
{"access_token":"74202587-6c2b-4d74-bcf2-0d687ea85dca","token_type":"bearer","expires_in":43199,"scope":"rest-api"}
然后,应将获得的访问令牌传递给 REST API,如文档中所述。
3.9.11.3.1. REST API 中的社交账号登录
-
在 web 模块的根包下创建
restapi
包,并在其中实现自定义 Spring MVC 控制器。该控制器应包含两个主要方法:get()
方法获取ResponseEntity
实例,login()
方法获取 OAuth 令牌。@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); }
在这里,我们检查 Facebook code,获取访问代码并使用
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); }
-
从 web/core 模块扫描中排除
restapi
包:OAuthTokenIssuer
bean 仅在 REST API 上下文中可用,在应用程序上下文中对其进行扫描将导致错误。<context:component-scan base-package="com.company.demo"> <context:exclude-filter type="regex" expression="com\.company\.demo\.restapi\..*"/> </context:component-scan>
-
在项目的
modules/web/web/VAADIN
文件夹中创建facebook-login-demo.html
文件,包含在 HTML 页面上运行的 JavaScript 代码:<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>
以下脚本将尝试使用 Facebook 登录。首先,它将从 URL 中删除 code 参数,然后它将 code 传递给 REST API 以获取 OAuth 访问令牌,在成功验证的情况下,将能够正常加载和保存数据。
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();
另一个示例或在 CUBA 应用程序中运行 JavaScript 代码,可以在JavaScript 用法示例部分找到。
3.9.11.4. 获取实体实例列表
假设系统有一个 sales$Order
实体,我们需要得到这个实体实例的列表。还有,我们需要得到的不是所有记录,而只是从第 100 条记录开始的 50 条记录。响应必须不仅包含 sales$Order
实体的简单属性,还包含有关客户的信息(名为 customer
的引用字段)。订单必须按日期排序。
获取 sales$Order
实体的所有实例的基本 URL 如下:
http://localhost:8080/app/rest/v2/entities/sales$Order
要实现上述所有条件,必须指定以下请求参数:
-
view - 一个用于加载实体的视图。在我们的例子中,
order-edit-view
包含一个customer
引用。 -
limit - 要返回的实体数。
-
offset - 第一个被提取记录的位置。
-
sort - 将用于排序的实体属性名称。
必须将 OAuth 令牌放在带有 Bearer
类型的 Authorization
请求头中:
Authorization: Bearer 29bc6b45-83cd-4050-8c7a-2a8a60adf251
这样,我们获得以下 GET 请求 URL:
http://localhost:8080/app/rest/v2/entities/sales$Order?view=order-edit-view&limit=50&offset=100&sort=date
使用 cURL,请求将如下所示:
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
响应将是这样的:
[
{
"_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"
}
}
]
请注意,响应中的每个实体都有一个带有实体名称的 _entityName
属性和一个带有实体实例名的 _instanceName
属性。
3.9.11.5. 新建实体实例
可以使用 POST 请求地址创建新的 sales$Order
实体实例:
http://localhost:8080/app/rest/v2/entities/sales$Order
必须将 OAuth 令牌放在带有 Bearer
类型的 Authorization
请求头中。
请求体必须包含一个描述新实体实例的 JSON 对象,例如:
{
"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"
}
}
以下是 cURL POST 请求的示例,该请求创建了一个新的 Order
实例:
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
订单项(items
)集合和 customer
的引用需要通过请求体传递。我们来看看如何处理这些属性。
首先,我们快速浏览一下 Order
类:
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
}
items
集合属性使用@Composition注解。用于实体创建和更新的 REST API 方法将为此类集合的所有成员创建新的实体实例。在我们的例子中,将使用 Order
实体创建两个 OrderItem
实体实例。
customer
引用没有 @Composition
注解,这也是为什么 REST API 会尝试查找具有给定 id 的客户并将其设置给 customer
字段。如果未找到客户,则不会创建订单,并且该方法将返回错误。
在成功执行方法的情况下,返回所创建实体的完整对象图:
{
"_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"
}
}
3.9.11.6. 更新现有实体实例
可以使用 PUT 请求地址更新现有的 sales$Order
实体实例:
http://localhost:8080/app/rest/v2/entities/sales$Order/5d7ff8e3-7828-ba94-d6ba-155c5c4f2a50
这里查询的最后一部分是实体标识符。
必须将 OAuth 令牌放在带有 Bearer
类型的 Authorization
请求头中。
请求体必须包含一个只包含要更新的字段的 JSON 对象,例如:
{
"date": "2017-10-01",
"customer" : {
"id" : "5d111245-2ed0-abec-3bee-1a196da92e3e"
}
}
响应体将包含一个修改过的实体:
{
"_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"
}
}
3.9.11.7. 执行 JPQL 查询(GET)
在使用 REST API 执行查询之前,查询语句必须在配置文件中进行描述。需要在 web 模块的主包中创建 rest-queries.xml
文件(例如 com.company.sales
)。然后,必须在 web 模块的应用程序属性文件(web-app.properties)中定义该文件。
cuba.rest.queriesConfig = +com/company/sales/rest-queries.xml
rest-queries.xml
内容:
<?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>
要执行 JPQL 查询,必须执行以下 GET 请求:
http://localhost:8080/app/rest/v2/queries/sales$Order/ordersAfterDate?startDate=2016-11-01&endDate=2017-11-01
请求 URL 部分:
-
sales$Order
- 提取的实体名称。 -
ordersAfterDate
- 配置文件中的查询名称。 -
startDate
和endDate
- 带有具体值的请求参数。
必须将 OAuth 令牌放在带有 Bearer
类型的 Authorization
请求头中。
该方法返回提取的实体实例的 JSON 数组:
[
{
"_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"
}
}
]
Swagger 文档 中提供了可能的请求参数的完整列表。
3.9.11.8. 执行 JPQL 查询(POST)
也可以使用 POST HTTP 请求执行查询。当需要将集合作为查询参数值传递时,可以使用 POST 请求。在这种情况下,REST 查询配置文件中查询参数的类型必须以方括号结尾:java.lang.String[]
,java.util.UUID[]
等。
<?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>
查询参数值必须作为 JSON map 在请求体中传递:
{
"ids": ["c273fca1-33c2-0229-2a0c-78bc6d09110a", "e6c04c18-c8a1-b741-7363-a2d58589d800", "d268a4e1-f316-a7c8-7a96-87ba06afbbbd"],
"status": "ready"
}
POST 请求 URL:
http://localhost:8080/app/rest/v2/queries/sales$Order/ordersByIds?returnCount=true
3.9.11.9. 调用服务方法(GET)
假设系统中存在 OrderService
服务。实现如下:
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;
}
}
在使用 REST API 执行之前,必须在配置文件中允许服务方法通过 REST 调用。需要在 web 模块的主包中创建 rest-services.xml
文件(例如 com.company.sales
)。然后,必须在 web 模块的应用程序属性文件(web-app.properties)中定义该文件。
cuba.rest.servicesConfig = +com/company/sales/rest-services.xml
rest-services.xml
内容:
<?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>
要调用该服务的方法,可以执行以下 GET 请求:
http://localhost:8080/app/rest/v2/services/sales_OrderService/calculatePrice?orderNumber=00001
请求 URL 部分:
-
sales_OrderService
- 服务名。 -
calculatePrice
- 方法名。 -
orderNumber
- 带有具体值的参数名称。
必须将 OAuth 令牌放在带有 Bearer
类型的 Authorization
请求头中。
服务方法可以返回简单数据类型、实体、实体集合或可序列化 POJO 的结果。在我们的例子中返回 BigDecimal,因此响应体中只包含一个数字:
39.2
3.9.11.10. 调用服务方法(POST)
REST API 不仅可以执行具有简单数据类型参数的方法,还可以执行具有以下参数的方法:
-
实体
-
实体集合
-
可序列化的 POJO
假设我们在上一节中创建的 OrderService
中添加一个新方法:
@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
类如下所示:
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;
}
}
新方法在参数列表中有一个 Order
实体,并返回一个 POJO。
在使用 REST API 调用之前该方法必须允许 REST 调用,因此我们添加一条记录到 rest-services.xml
配置文件中(在 调用服务方法(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>
可以使用以下地址以 POST 请求调用 validateOrder
服务的方法:
http://localhost:8080/app/rest/v2/services/sales_OrderService/validateOrder
如果 POST 请求参数在请求体中传递。请求体必须包含一个 JSON 对象,此对象的每个字段对应于服务的方法参数。
{
"order" : {
"number": "00050",
"date" : "2016-01-01"
},
"validationDate": "2016-10-01"
}
必须将 OAuth 令牌放在带有 Bearer
类型的 Authorization
请求头中。
REST API 方法返回可序列化的 POJO:
{
"success": false,
"errorMessage": "Validation of order 00050 failed. validationDate parameter is: 2016-10-01"
}
3.9.11.11. 文件下载
当 下载文件 时,在请求头中传递安全令牌通常很不方便。用户更希望有一个用于下载的 URL,这个 URL 可以设置给 img 标签的 src 属性。
作为该问题的解决方案,OAuth 令牌还可以在请求 URL 中以 access_token 参数传递。
例如,图片被上传到应用程序。其 FileDescriptor 实例的 id 为 44809679-e81c-e5ae-dd81-f56f223761d6
。
在这种情况下,下载图像的 URL 如下所示:
http://localhost:8080/app/rest/v2/files/44809679-e81c-e5ae-dd81-f56f223761d6?access_token=a2f0bb4e-773f-6b59-3450-3934cbf0a2d6
3.9.11.12. 文件上传
要上传文件,应该先获得一个访问令牌,在后续请求中要使用这个令牌。
假设我们有如下文件上传表单:
<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"/>
我们将使用 jQuery 进行上传,并使用 data 获取返回的 JSON,其内容是为上传的文件新建的 FileDescriptor
实例。之后我们可以将访问令牌作为参数通过其 FileDescriptor
的 id 访问上传的文件:
$('#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();
}
});
});
3.9.11.13. JavaScript 用法示例
本节包含在 JavaScript 中使用 REST API v2 的示例,此处 JavaScript 运行在 HTML 页面中。该页面最初显示登录表单,登录成功后显示消息和实体列表。
为简单起见,我们将使用 modules/web/web/VAADIN
文件夹存储 HTML、CSS 和 JavaScript 文件,因为默认情况下,部署的 Web 应用程序的相应的文件夹用来提供静态资源。因此无需对 Tomcat 应用程序服务器进行任何配置。生成的 URL 将以 http://localhost:8080/app/VAADIN
开头,因此不要在真实环境的应用程序中使用此方法 - 而应该为静态资源创建一个具有特定上下文的单独 Web 应用程序。
下载 jQuery 和 Bootstrap 并复制到项目的 modules/web/web/VAADIN
文件夹。创建 customers.html
和 customers.js
文件,因此文件夹的内容应如下所示:
bootstrap.min.css
customers.html
customers.js
jquery-3.1.1.min.js
customers.html
文件内容:
<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
文件内容:
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>");
});
}
});
}
来自用户输入的登录名和密码通过 POST 请求发送到服务器,并在 Authorization
请求头中使用 Base64 编码的客户端凭证,如获取 OAuth 令牌部分中所述。如果验证成功,则网页从服务器接收访问令牌值,令牌存储在 oauthToken
变量中,之后,隐藏 loginForm
div 并显示 loggedInStatus
div。
需要显示客户列表,请求被发送到服务器去获取 sales$Customer
实体的实例,在 Authorization
请求头中传递 oauthToken
值。
如果成功处理请求,则显示 customers
div,并且 customersList
元素填充包含客户名称和电子邮件的内容。
3.9.11.14. 获取本地化消息
REST API 中有一些方法可以获取实体,及其属性和枚举的本地化消息。
例如,要获取 sec$User
实体的本地化消息列表,必须执行以下 GET 请求:
http://localhost:8080/app/rest/v2/messages/entities/sec$User
必须将 OAuth 令牌放在带有 Bearer
类型的 Authorization
请求头中。
可以使用 Accept-Language http 请求头显式指定所需的区域设置。
响应如下:
{
"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"
}
要获取枚举的本地化值,请使用以下 URL:
http://localhost:8080/app/rest/v2/messages/enums/com.haulmont.cuba.security.entity.RoleType
如果省略 URL 中的实体名称或枚举名称部分,将获得所有实体或枚举的本地化值。
3.9.11.15. 数据模型版本化示例
- 实体属性被重命名
-
我们假设
sales$Order
实体的oldNumber
属性被重命名为newNumber
并且date
被重命名为deliveryDate
。在这种情况下,转换配置将如下所示:<?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>
如果客户端应用程序需要使用旧版的
sales$Order
实体,那么它必须在 URL 参数中传递modelVersion
值:http://localhost:8080/app/rest/v2/entities/sales$Order/c838be0a-96d0-4ef4-a7c0-dff348347f93?modelVersion=1.0
将返回以下结果:
{ "_entityName": "sales$Order", "_instanceName": "00001", "id": "46322d73-2374-1d65-a5f2-160461da22bf", "date": "2016-10-31", "description": "Vacation order", "oldNumber": "00001" }
响应 JSON 包含
oldNumber
和date
属性,尽管 CUBA 应用程序中的实体具有newNumber
和deliveryDate
属性。 - 实体名称被更改
-
接下来,试想一下,在应用程序的某个版本中,
sales$Order
实体的名称也发生了变化。新名称是sales$NewOrder
。版本
1.1
的转换配置将如下所示:<?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>
除了上一个示例中的配置之外,还在此处添加了
oldEntityName
属性。它指定对模型版本1.1
有效的实体名称。currentEntityName
属性指定当前实体名称。虽然名称为
sales$Order
的实体不再存在,但以下请求也可以正常运行:http://localhost:8080/app/rest/v2/entities/sales$Order/c838be0a-96d0-4ef4-a7c0-dff348347f93?modelVersion=1.1
REST API 控制器将认为它必须在
sales$NewOrder
实体里进行搜索,并且在找到具有给定 id 的实体之后,在结果 JSON 中替换实体的名称和newNumber
属性的名称:{ "_entityName": "sales$Order", "_instanceName": "00001", "id": "46322d73-2374-1d65-a5f2-160461da22bf", "date": "2016-10-31", "description": "Vacation order", "oldNumber": "00001" }
客户端应用程序也可以使用旧版本的数据模型进行实体更新和创建。
此 POST 请求使用旧实体名称,并且在请求体使用旧的 JSON,这个 POST 请求可以正常工作:
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" }
- 必须从 JSON 中删除的实体属性
-
如果某个属性已添加到实体,但使用旧版数据模型的客户端不希望有此新属性,则可以从结果 JSON 中删除新属性。
此示例的转换配置如下所示:
<?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>
此配置文件中的转换包含带有嵌套
removeAttribute
命令的toVersion
标记。这意味着当执行从当前状态到特定版本的转换时(即当请求实体列表时),必须从结果 JSON 中删除discount
属性。在这种情况下,如果在没有
modelVersion
属性的情况下执行请求,discount 属性将会被返回: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 }
如果指定
modelVersion
,则将删除discount
属性http://localhost:8080/app/rest/v2/entities/sales$Order/c838be0a-96d0-4ef4-a7c0-dff348347f93?modelVersion=1.1
{ "_entityName": "sales$Order", "_instanceName": "00001", "id": "46322d73-2374-1d65-a5f2-160461da22bf", "deliveryDate": "2016-10-31", "description": "Vacation order", "oldNumber": "00001" }
- 使用自定义转换器
-
还可以创建和注册自定义 JSON 转换器。举个例子,我们来看看下面的情况:有一个实体
sales$OldOrder
被重命名为sales$NewOrder
。该实体具有orderDate
字段。在之前的版本中,此日期字段包含时间部分,但在最新版本的实体中,时间部分将被删除。请求旧模型版本1.0
的实体的 REST API 客户端期望日期字段有时间部分,因此转换器必须修改 JSON 中的值。首先,转换器配置必须如下所示:
<?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>
有一个
custom
元素和嵌套的toVersion
和fromVersion
元素。这些元素引用了转换器 bean。这意味着自定义转换器必须注册为 Spring bean。这里有一件重要的事情:自定义转换器可以使用RestTransformations
平台 bean(如果需要,这个 bean 可以访问其它实体转换器)。但是RestTransformations
bean 在 REST API servlet 的 Spring 上下文中注册,而不是在 Web 应用程序的主上下文中注册。这意味着自定义转换器 bean 也必须在 REST API Spring 上下文中注册。我们怎样做到这一点?
首先,在 web 或 portal 模块中创建一个
rest-dispatcher-spring.xml
(例如,在com.company.test
包中)。接下来,在 Web 或 portal 模块的
app.properties
中注册此文件:cuba.restSpringContextConfig = +com/company/test/rest-dispatcher-spring.xml
rest-dispatcher-spring.xml
必须包含自定义转换器 bean 定义:<?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>
sales_OrderJsonTransformerToVersion
转换器的内容如下: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"); } } }
此转换器在 JSON 对象中找到
orderDate
节点,并通过将时间部分添加到该值来修改该值。当请求数据模型版本
1.0
的sales$OldOrder
实体时,结果 JSON 将包括含有时间部分的orderDate
字段的实体,尽管它不再存储在数据库中。关于定制转换器的更多内容:它们必须实现
EntityJsonTransformer
接口。还可以继承AbstractEntityJsonTransformer
类并覆盖其doCustomTransformations
方法。AbstractEntityJsonTransformer
类包含标准转换器的所有功能。
3.9.11.16. 使用实体搜索过滤器
REST API 可以在获取实体列表时可以指定即时搜索条件。
假设我们有两个实体:
-
Author 有两个字段:
lastName
和firstName
-
Book 有三个字段:
title
(String)、author
(Author) 和publicationYear
(Integer)
要使用条件执行搜索,必须使用以下 URL:
http://localhost:8080/app/rest/v2/entities/test$Book/search
搜索条件需要在 filter
参数中传递。这是一个包含一组条件的 JSON 对象。如果使用 GET 请求执行搜索,则需要在 URL 中传递 filter
参数。
- 例 1
-
我们需要找到 2007 年出版的所有书籍,并且有一个名字以"Alex"开头的作者。过滤器 JSON 应如下所示:
{
"conditions": [
{
"property": "author.firstName",
"operator": "startsWith",
"value": "Alex"
},
{
"property": "publicationDate",
"operator": "=",
"value": 2007
}
]
}
默认情况下,搜索条件之间使用 AND 连接。
此示例还演示了对嵌套属性(author.firstName
)的支持。
- 例 2
-
下一个示例演示了两件事:如何使用 POST 请求执行搜索以及如何使用 OR 分组条件。如果是 POST 请求,则必须在请求体的 JSON 对象中传递所有参数。搜索过滤器必须放在名为
filter
的对象字段中。所有其它参数(视图名称、限制条件等)必须放在相应名称的字段中:
{
"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"
}
在此示例中,conditions
集合不仅包含条件对象,还包含 OR 分组条件。所以最终搜索条件将是:
((author.lastName contains Stev) OR (author.lastName = Duma) AND (publicationDate in [2007, 2008]))
请注意,view
参数也在请求体中传递。
3.10. 平台功能
本节介绍平台提供的各种可选功能
3.10.1. 动态属性
Dynamic attributes - 动态属性 是额外的实体属性,可以在不需要修改数据库表结构或者重启应用的情况下为实体添加。动态属性通常用来在部署或者生产阶段为实体定义新属性。
CUBA 动态属性实现了 Entity-Attribute-Value 模型。
-
Category
- 定义对象的一个 category - 类别 以及相应的一组动态属性。这个类别必须分配给某些实体类型。比如,有个实体是 Car 类型的。可以为它定义两个类别:卡车(Truck)和客车(Passenger)。卡车类别需要包含载重量和车身类型属性,客车类别需要包含座位数和儿童座位属性。
-
CategoryAttribute
- 定义关联某些类别的动态属性。每个属性需要为一个明确类型描述一个字段。必要的Code
字段包含属性的系统名称。Name
字段包含具有可读性的名称。 -
CategoryAttributeValue
- 特定实体实例动态属性的值。动态属性值物理存储在专门的SYS_ATTR_VALUE
表内。表的每一行都有指向某些实体的 id(ENTITY_ID
列)。
一个实体实例可以拥有跟这个实体类型相关的所有类别的动态属性。所以如果按照上面创建了两个关于 Car 的类别,则可以为一个 Car 实例定义两种类别中的任何动态属性。如果需要将实体实例分类到具体的某一类别(比如 Car 可以是卡车或者客车),那么实体需要实现 Categorized 接口。这样实体实例就会有指向类别的引用,也只能包含此类别的动态属性。
加载和保存动态属性是通过 DataManager 来处理的。LoadContext.setLoadDynamicAttributes()
方法可以用来设置是否需要为实体实例加载动态属性。默认情况下,不会加载动态属性。同时,DataManager
总是会保存传递给 commit()
方法的实体实例的动态属性。
对于任何继承自 BaseGenericIdEntity
的持久化实体,动态属性的值可以通过 getValue()
/ setValue()
方法来读写。此时,需要给方法传递一个带 +
前缀的属性 code。示例:
LoadContext lc = new LoadContext(Car.class).setId(id);
lc.setLoadDynamicAttributes(true);
Entity entity = dataManager.load(lc);
Double capacity = entity.getValue("+loadCapacity");
entity.setValue("+loadCapacity", capacity + 10);
dataManager.commit(entity);
3.10.1.1. 管理动态属性
管理属性类别和描述是通过 Administration>Dynamic Attributes 菜单中提供的特定界面完成的。
类别浏览界面显示所有已注册类别的列表。
类别编辑界面可以为实体创建新类别并定义一组动态属性。类别名称和相关实体类型字段是必填字段。Default 复选框表示将为实现了 Categorized
接口的新实体实例自动选择此类别。
如果应用程序支持多种语言,则会显示 Name localization 分组框。它允许为每个可用的语言环境设置类别的本地化名称。
动态属性编辑界面可以设置属性的名称、系统代码、值类型和默认值。
对于除 Boolean
以外的所有值类型,都有一个 Width 字段可用于设置 FieldGroup
中的字段宽度(以像素为单位或百分比)。如果 Width 字段为空,则假定其值为 100%。
对于除 Boolean
和 Enumeration
之外的所有值类型,还有一个 Is collection 复选框。允许为所选类型创建多值动态属性。
所有类型的动态属性都支持本地化:
对于 Enumeration
值类型,通过列表编辑器在 Enumeration 字段中定义命名值集合。
Enumeration
类型的动态属性编辑界面
每个枚举值也可以进行本地化显示设置。
Enumeration
类型动态属性本地化设置
动态属性还可以设置可见性,定义在哪些界面中显示。默认情况下,动态属性在任何界面上都是不可见的。
除了界面之外,还可以为属性指定显示组件(比如,可以在界面中,指定多个FieldGroup组件显示同一实体的字段)。
如果该属性在界面上标记为可见,则在界面上用来展示相应实体的所有字段组(FieldGroup)和表格(Table)中会自动显示该属性。
对动态属性的访问也受用户角色设置的限制。动态属性的安全设置与常规属性的安全设置类似。
要使属性和可见性设置的更改生效,请单击类别浏览界面中的 Apply settings。也可以通过在 Administration>JMX Console 中调用 app-core.cuba:type = CachingFacade
的 JMX bean 的 clearDynamicAttributesCache()
方法使更改生效。
通过可见性配置能将动态属性自动添加到界面,如下所示:
也可以手动将动态属性添加到界面。操作步骤如下:
-
在界面 XML 描述的
dsContext
部分,为加载实体的数据源将loadDynamicAttributes
属性设置为true
,例如:<dsContext> <datasource id="carDs" class="com.company.sample.entity.Car" view="_local" loadDynamicAttributes="true"/> </dsContext>
-
在组件定义的
property
XML 属性中使用+
前缀指定动态属性 code:<textField id="numberOfSeats" datasource="carDs" property="+numberOfSeats"/>
3.10.1.2. 实体分类
如果实体实现了 com.haulmont.cuba.core.entity.Categorized
接口,则可以使用 com.haulmont.cuba.gui.components.RuntimePropertiesFrame
组件来显示该实体的动态属性。此组件允许用户为特定实体实例选择类别,并指定此类别的动态属性的值。
要在编辑界面中使用 RuntimePropertiesFrame
组件,请执行以下操作:
-
在
dsContext
部分声明两个datasources:-
runtimePropsDatasource
- 加载CategoryAttributeValue
实例的特定数据源。mainDs
属性必须引用主数据源,其中包含被编辑的实体。 -
一个常规的
collectionDatasource
来加载这个实体类型的类别列表。
示例:
<dsContext> <datasource id="carDs" class="com.company.sample.entity.Car" view="carEdit"/> <runtimePropsDatasource id="runtimePropsDs" mainDs="carDs"/> <collectionDatasource id="categories" class="com.haulmont.cuba.core.entity.Category" view="_local"> <query> select c from sys$Category c where c.entityType='sample$Car' </query> </collectionDatasource> </dsContext>
-
-
现在,数据源配置好了,可以将
runtimeProperties
可视化组件添加在界面的 XML 描述中:<runtimeProperties id="runtimePropsFrame" runtimeDs="runtimePropsDs" categoriesDs="categories"/>
3.10.2. 发送邮件
平台的电子邮件发送提供以下基础功能:
-
同步或异步发送。在同步发送的情况下,调用代码将一直等待,直到消息被发送到 SMTP 服务器。在异步发送的情况下,消息被持久化到数据库,并且将控制权立即交回给调用代码。实际发送动作稍后由计划任务完成。
-
对于同步和异步模式,都能可靠地跟踪消息发送的时间戳或数据库中的错误。
-
提供用户界面,用于搜索和查看有关已发送消息的信息,包括所有消息属性和内容、发送状态和尝试次数。
3.10.2.1. 发送方法
要发送电子邮件,在中间件上应使用 EmailerAPI
bean,在客户端层使用 EmailService
服务。
这些组件的基本方法如下所述:
-
sendEmail()
– 同步发送消息。调用代码在消息发送到 SMTP 服务器前一直等待。消息内容由一组参数(逗号分隔的收件人列表,主题,内容,附件数组)组成,并以
EmailInfo
对象的形式传输,该对象封装了所有这些信息并允许显式设置发件人的地址并使用 FreeMarker 模板构造邮件正文。同步发送时可能会抛出
EmailException
,其中包含投递失败的收件人地址的信息,以及相应的错误消息。执行该方法期间,在数据库中为每个收件人创建一个
SendingMessage
实例。发送状态初始值为SendingStatus.SENDING
,成功发送后状态变为SendingStatus.SENT
。如果消息发送错误,消息状态将更改为SendingStatus.NOTSENT
。 -
sendEmailAsync()
- 异步发送消息。此方法返回在数据库中创建的SendingStatus.QUEUE
状态的SendingMessage
实例的列表(列表数量按收件人数)。实际发送是通过随后调用EmailerAPI.processQueuedEmails()
方法执行的,需要为 计划任务设置符合期望的发送邮件频率来调用此方法。
3.10.2.2. 电子邮件附件
EmailAttachment
对象是一个包装器,包含附件的信息:字节数组( data
字段)、文件名( name
字段),如有必要,还可在包含邮件中可以使用的附件唯一标识(contentId
字段,虽然是可选字段,但很有用)。
附件标识可用于在邮件消息体中插入图片。为此,在创建 EmailAttachment
时指定了唯一的 contentId
(例如,myPic
)。使用 cid:myPic
格式的表达式可以在消息体中插入附件路径。因此,要插入图片,可以指定以下 HTML 元素:
<img src="cid:myPic"/>
3.10.2.3. 配置电子邮件发送参数
使用下面列出的应用程序属性配置电子邮件发送参数。它们都是运行时参数并存储在数据库中,但对于特定的 Middleware 模块可以在其 app.properties
文件覆盖这些参数。
所有电子邮件发送参数均可通过 EmailerConfig
配置接口获取。
-
cuba.email.fromAddress
– 默认发件人地址。如果没有指定EmailInfo.from
属性时使用它。默认值:
DoNotReply@localhost
-
cuba.email.smtpHost
– SMTP 服务器的地址。默认值:
test.host
-
cuba.email.smtpPort
– SMTP 服务器的端口。默认值:
25
-
cuba.email.smtpAuthRequired
标记 SMTP 服务器是否需要身份认证。对应mail.smtp.auth
参数,该参数在创建javax.mail.Session
对象时传递。默认值:
false
-
cuba.email.smtpSslEnabled
标记 SMTP 是否启用了SSL
协议。对应于带有smtps
值的mail.transport.protocol
参数,该值在创建javax.mail.Session
对象时传递。默认值:
false
-
cuba.email.smtpStarttlsEnable
– 标记在 SMTP 服务器上进行身份验证时使用STARTTLS
命令。对应mail.smtp.starttls.enable
参数,该参数在创建javax.mail.Session
对象时传递。默认值:
false
-
cuba.email.smtpUser
– SMTP 服务器身份验证的用户名。
-
cuba.email.smtpPassword
– SMTP 服务器身份验证的用户密码。
-
cuba.email.delayCallCount
– 用于邮件的异步发送,以便在服务器启动后跳过对EmailManager.queueEmailsToSend()
的前几次调用,这样可以减少应用程序初始化期间的负荷。电子邮件发送将从下一次调用开始。默认值:
2
-
cuba.email.messageQueueCapacity
– 用于异步发送,从队列中读取并在调用一次EmailManager.queueEmailsToSend()
时发送的最大消息数。默认值:
100
-
cuba.email.defaultSendingAttemptsCount
用于异步发送,发送电子邮件的默认尝试次数。如果在调用Emailer.sendEmailAsync()
时未指定attemptsCount
参数则使用它。默认值:
10
-
cuba.email.maxSendingTimeSec
– 将电子邮件发送到 SMTP 服务器所需的最长预期时间(以秒为单位)。用于异步发送,优化从 DB 队列选择SendingMessage
对象。默认值:120
-
cuba.email.sendAllToAdmin
– 表示无论指定的收件人地址如何指定,都应将所有消息发送到 cuba.email.adminAddress 地址。建议在系统开发和调式期间使用此参数。默认值:
false
-
cuba.email.adminAddress
– 如果启用cuba.email.sendAllToAdmin
属性,则所有消息都会被发送到这个地址。默认值:
admin@localhost
-
cuba.emailerUserLogin
–系统用户的登录名,由异步电子邮件发送代码使用,以便能够将信息保存到数据库中。建议创建没有密码的单独的用户(例如,emailer
),这样的话此用户不能在用户界面使用用户名登录。这也便于在服务端日志中搜索与电子邮件发送相关的消息。默认值:
admin
-
cuba.email.exceptionReportEmailTemplateBody
- 异常报告邮件正文的*.gsp
模板路径。模板基于 Groovy 的
SimpleTemplateEngine
语法,因此可以在模板内容中使用 Groovy 代码块:-
toHtml()
方法通过转义和替换特殊符号将字符串转换为 HTML 字符串, -
timestamp
- 最后一次尝试发送电子邮件的日期, -
errorMessage
- 错误消息, -
stacktrace
- 错误的堆栈跟踪。
模板文件的示例:
<html> <body> <p>${timestamp}</p> <p>${toHtml(errorMessage)}</p> <p>${toHtml(stacktrace)}</p> </body> </html>
-
-
cuba.email.exceptionReportEmailTemplateSubject
- 异常报告邮件主题的*.gsp
模板路径。模板文件的示例:
[${systemId}] [${userLogin}] Exception Report
还可以使用 JavaMail API 中的属性,将它们添加到 core 模块的 app.properties
文件中。在创建 javax.mail.Session
对象时将传递 mail.*
属性。
可以使用 app-core.cuba:type=Emailer
JMX bean 查看当前参数值并发送测试消息。
3.10.2.4. 发送电子邮件
本节包含使用 CUBA 邮件发送机制发送电子邮件的实用指南。
我们看看以下任务:
-
有
NewsItem
实体和NewsItemEdit
界面。 -
NewsItem
实体包含以下属性:date
、caption
、content
。 -
我们希望每次通过
NewsItemEdit
界面创建新的NewsItem
实例时向某些地址发送电子邮件。电子邮件应包含NewsItem.caption
作为主题,并且应该从包含NewsItem.content
的模板创建邮件消息正文。
-
将以下代码添加到
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); } }
在上面的代码中,在
sendByEmail()
方法中调用EmailService
并传递描述邮件消息的EmailInfo
实例给sendEmailAsync
方法。邮件消息的主体将基于news_item.txt
模板创建。 -
在 core 模块的
com.company.demo.templates
包中创建邮件消息主体模板文件news_item.txt
。The company news: ${newsItem.content}
这是一个 Freemarker 模板,其使用
EmailInfo
实例中传递的参数(在本例中为newsItem
)。 -
运行应用程序,打开
NewsItem
实体浏览界面并点击 Create。编辑界面将被打开。填写字段并点击 OK。将显示一个确认对话框,询问是否发送邮件。点击 Yes。 -
切换到应用程序的 Administration > Email History 界面。将看到两个处于
Queue
状态的记录(按收件人数量)。这表示电子邮件在队列中但尚未发送。 -
要处理队列,请设置计划任务。切换到应用程序的 Administration > Scheduled Tasks 界面。创建一个新任务并设置以下参数:
-
Bean Name -
cuba_Emailer
-
Method Name -
processQueuedEmails()
-
Singleton - yes(这对于中间层服务集群很重要)
-
Period, sec - 10
保存任务并点击 Activate。
如果之前没有为此项目设置定时任务的执行,则此阶段不会发生任何事情 - 在启动整个定时机制之前,任务不会执行。
-
-
打开
modules/core/src/app.properties
文件并添加以下 属性:cuba.schedulingActive = true
重启应用服务。定时机制现在处于激活状态并调用电子邮件队列处理程序。
-
转到 Administration > Email History 界面。如果发送成功,电子邮件的状态将为
Sent
。否则最有可能为Sending
或Queue
。在后一种情况下,可以在build/tomcat/logs/app.log
中打开应用程序日志并找出原因。电子邮件发送机制将尝试多次(默认为 10 次)发送邮件消息,如果失败,设置状态为Not sent
。 -
无法发送电子邮件的最常见原因是没有设置 SMTP 服务器参数。可以通过
app-core.cuba:type=Emailer
JMX bean 或中间件中的应用程序属性文件中设置参数。我们看看后者。打开modules/core/src/app.properties
文件并添加所需的参数。cuba.email.fromAddress = do-not-reply@company.com cuba.email.smtpHost = mail.company.com
重启应用程序服务。转到 Administration > JMX Console,找到
Emailer
JMX bean 并尝试使用sendTestEmail()
操作向自己发送测试邮件。 -
现在发送机制已经正确设置,但它不会发送在
Not sent
状态中的邮件消息。所以必须在编辑界面中创建另一个NewsItem
。执行此操作然后观察在 Email History 界面中新邮件消息的状态如何更改为Sent
。
3.10.3. 实体探查器
实体探查器可以在任何应用程序对象上使用,而无需创建专用界面。探查器动态生成界面来浏览和编辑所选的实体实例。
这使系统管理员有机会查看和编辑由于设计原因而无法从标准界面访问的数据,并能在原型设计阶段创建数据模型以及创建仅链接到实体探查器的主菜单部分。
探查器的入口是 com/haulmont/cuba/gui/app/core/entityinspector/entity-inspector-browse.xml
界面。
如果使用名为 entity
的 String
类型参数将实体名称作为参数传递给实体探查器,则探查器将显示具有过滤、选择和编辑功能的实体列表。在screens.xml中配置界面时可以指定参数,例如:
screens.xml
<screen id="sales$Product.lookup"
template="/com/haulmont/cuba/gui/app/core/entityinspector/entity-inspector-browse.xml">
<param name="entity"
value="sales$Product"/>
</screen>
menu.xml
<item id="sales$Product.lookup"/>
界面标识符定义为 {entity_name}.lookup
时将允许PickerField和LookupPickerField组件在 PickerField.LookupAction
标准操作中使用此界面。
通常可以在没有任何参数的情况下调用界面。在这种情况下,界面顶部将包含一个实体选择字段。在 cuba 应用程序组件中,探查器界面使用 entityInspector.browse
标识符进行注册,因此在菜单项中可以很容易引用:
<item id="entityInspector.browse"/>
3.10.4. 实体日志
此机制在实体监听器级别跟踪实体的持久化,即确保能跟踪所有经过EntityManager的持久化上下文的数据库变更。但是不跟踪使用 SQL 对数据库实体的直接更改,包括使用NativeQuery或QueryRunner执行的更改。
修改后的实体实例在保存到数据库之前传递给 EntityLogAPI
bean 的 registerCreate()
、registerModify()
和 registerDelete()
方法。每个方法都有 auto
参数,通过这个参数控制实体监听器添加的自动日志与通过从应用程序代码调用这些方法添加的手动日志分离。当从实体监听器调用这些方法时,auto
参数的值为 true
。
日志包含有关修改时间、修改实体的用户以及修改后属性新值的信息。日志实体存储在与 EntityLogItem
实体对应的 SEC_ENTITY_LOG 表中。更改的属性值存储在 CHANGES 列中,在中间件加载时,将属性转换为 EntityLogAttr
实体的实例。
3.10.4.1. 配置实体日志
配置实体日志的最简单方法是使用 Administration > Entity Log > Setup 应用程序界面。
如果要将配置包含在数据库初始化脚本中,还可以通过在数据库中添加一些记录来配置实体日志。
使用 LoggedEntity
实体和 LoggedAttribute
实体配置日志记录,分别对应于数据库的 SEC_LOGGED_ENTITY 表和 SEC_LOGGED_ATTR 表。
LoggedEntity
定义了需要记录日志的实体类型。LoggedEntity
具有以下属性:
LoggedAttribute
定义要记录的实体属性,并包含指向 LoggedEntity
的链接和属性名称。
要为某个实体配置日志记录,应将相应的配置项添加到 SEC_LOGGED_ENTITY 和 SEC_LOGGED_ATTR 表中。例如,使用以下语句将记录 Customer
实体的 name
和 grade
属性的更改:
insert into SEC_LOGGED_ENTITY (ID, CREATE_TS, CREATED_BY, NAME, AUTO, MANUAL)
values ('25eeb644-e609-11e1-9ada-3860770d7eaf', now(), 'admin', 'sales$Customer', true, true);
insert into SEC_LOGGED_ATTR (ID, CREATE_TS, CREATED_BY, ENTITY_ID, NAME)
values (newid(), now(), 'admin', '25eeb644-e609-11e1-9ada-3860770d7eaf', 'name');
insert into SEC_LOGGED_ATTR (ID, CREATE_TS, CREATED_BY, ENTITY_ID, NAME)
values (newid(), now(), 'admin', '25eeb644-e609-11e1-9ada-3860770d7eaf', 'grade');
默认情况下会激活日志记录机制。如果要停止它,请设置 app-core.cuba:type=EntityLog
JMX bean 的 Enabled
属性为 false
,然后调用其 invalidateCache()
方法。或者,将cuba.entityLog.enabled应用程序属性设置为 false
并重新启动服务。
3.10.4.2. 查看实体日志
实体日志内容可以在 Administration > Entity Log 上的专用界面上查看。
除此之外,也能在其它应用程序界面访问实体更改日志,只要加载 EntityLogItem
集合及其关联的 EntityLogAttr
实例到数据容器,再创建连接到这些数据容器的可视化组件。
下面的例子展示了 Customer
实体界面的 XML 描述片段,这里有一个带有实体日志内容的标签页。
<data>
<instance id="customerDc"
class="com.company.sample.entity.Customer"
view="customer-view">
<loader id="customerDl"/>
</instance>
<collection id="entitylogsDc"
class="com.haulmont.cuba.security.entity.EntityLogItem"
view="logView" >
<loader id="entityLogItemsDl">
<query><![CDATA[select i from sec$EntityLog i where i.entityRef.entityId = :customer
order by i.eventTs]]>
</query>
</loader>
<collection id="logAttrDc"
property="attributes"/>
</collection>
</data>
<layout>
<tabSheet id="tabSheet">
<tab id="propertyTab">
<!--...-->
</tab>
<tab id="logTab">
<table id="logTable"
dataContainer="entitylogsDc"
width="100%"
height="100%">
<columns>
<column id="eventTs"/>
<column id="user.login"/>
<column id="type"/>
</columns>
</table>
<table id="attrTable"
height="100%"
width="100%"
dataContainer="logAttrDc">
<columns>
<column id="name"/>
<column id="oldValue"/>
<column id="value"/>
</columns>
</table>
</tab>
</tabSheet>
</layout>
看看 Customer
界面控制器:
@UiController("sample_Customer.edit")
@UiDescriptor("customer-edit.xml")
@EditedEntityContainer("customerDc")
public class CustomerEdit extends StandardEditor<Customer> {
@Inject
private InstanceLoader<Customer> customerDl;
@Inject
private CollectionLoader<EntityLogItem> entityLogItemsDl;
@Subscribe
private void onBeforeShow(BeforeShowEvent event) { (1)
customerDl.load();
}
@Subscribe(id = "customerDc", target = Target.DATA_CONTAINER)
private void onCustomerDcItemChange(InstanceContainer.ItemChangeEvent<Customer> event) { (2)
entityLogItemsDl.setParameter("customer", event.getItem().getId());
entityLogItemsDl.load();
}
}
注意该界面并没有 @LoadDataBeforeShow
注解,因为加载数据是显式触发的。
1 | − onBeforeShow 方法在界面展示前加载数据。 |
2 | − 在 customerDc 容器的 ItemChangeEvent 处理器中,为依赖的加载器设置了参数并触发加载。 |
要显示本地化的值,启用日志记录的属性应包含@LocalizedValue注解。有此注解时,日志记录机制将填写 EntityLogAttr.messagesPack
字段,从而上面示例中的表格可以使用 locValue
列代替 value
列来显示本地化值:
<table id="attrTable" width="100%" height="200px" dataContainer="logAttrDc">
<columns>
<column id="name"/>
<column id="locValue"/>
</columns>
</table>
3.10.5. 实体快照
实体保存机制与实体日志非常相似,目的在于跟踪运行时的数据变化,具有以下独特的特征:
-
能保存通过特定视图定义的整个实体关系图的状态(或快照)。
-
应用程序代码能显式调用快照保存机制。
-
平台允许查看和比较实体快照。
3.10.5.1. 保存快照
为了保存给定实体关系图的快照,需要调用 EntitySnapshotService.createSnapshot()
方法,并至少传递两个参数 - 实体和视图,实体是对象关系图入口点实体,视图用于描述关系图。将会使用已加载的实体创建快照,不做任何对数据库的调用。因此,如果加载实体的视图中不包含某些字段,快照也不会包含这些字段。
Java 的对象图被转换为 XML 并与主实体的链接一起保存在 SYS_ENTITY_SNAPSHOT 表(对应 EntitySnapshot
实体)中。
通常,在编辑界面提交后需要保存快照。可以通过重写界面控制器的 postCommit()
方法来实现这种需求。例如:
public class CustomerEditor extends AbstractEditor<Customer> {
@Inject
protected Datasource<Customer> customerDs;
@Inject
protected EntitySnapshotService entitySnapshotService;
...
@Override
protected boolean postCommit(boolean committed, boolean close) {
if (committed) {
entitySnapshotService.createSnapshot(customerDs.getItem(), customerDs.getView());
}
return super.postCommit(committed, close);
}
}
3.10.5.2. 查看快照
使用 com/haulmont/cuba/gui/app/core/entitydiff/diff-view.xml
子框架可以查看任意实体的快照。例如:
<frame id="diffFrame"
src="/com/haulmont/cuba/gui/app/core/entitydiff/diff-view.xml"
width="100%"
height="100%"/>
快照应该通过编辑界面控制器加载到框架中:
public class CustomerEditor extends AbstractEditor<Customer> {
@Inject
private EntityStates entityStates;
@Inject
protected EntityDiffViewer diffFrame;
...
@Override
protected void postInit() {
if (!entityStates.isNew(getItem())) {
diffFrame.loadVersions(getItem());
}
}
}
diff-view.xml
子框架显示给定实体的快照列表,并能够对它们进行比较。每一个快照视图包含用户、日期和时间。当从列表中选中一个快照,将显示与上一个快照相比的变化。第一个快照的所有属性被标记为已更改。选择两个快照在表格中展示比较的结果。
比较结果表展示属性名称及其新值。当一行被选中,将显示两个快照上属性更改的详细信息。引用字段则会显示相应实体的实例名。 当比较集合时,新元素和删除的元素分别以高亮的绿色和红色显示。只有属性发生更改的集合元素不会高亮显示。不记录元素位置的改变。
3.10.6. 实体统计
实体统计机制提供了数据库中当前实体实例数量的信息。此数据用于自动为关联实体选择最佳查找策略,并限制 UI 界面中显示的搜索结果的数量。
统计信息存储在 SYS_ENTITY_STATISTICS
表中,该表映射到 EntityStatistics
实体。可以使用PersistenceManagerMBean JMX bean 的 refreshStatistics()
方法自动更新。如果将实体名称作为参数传递,则将收集给定实体的统计信息,否则为所有实体收集统计信息。如果要定期更新统计信息,可以创建调用此方法的计划任务。请注意,收集数据过程将为每个实体执行 select count(*)
,这样会增加对数据库的压力。
可以通过中间层的 PersistenceManagerAPI
接口和客户端上的 PersistenceManagerService
来以编程方式访问实体统计信息。统计信息缓存在内存中,因此只有在服务器重启之后或在调用 PersistenceManagerMBean.flushStatisticsCache()
方法之后,对数据库中统计信息的直接更改才会生效。
EntityStatistics
有如下属性:
-
name
(NAME
列) – 实体元类名称,例如sales$Customer
。 -
instanceCount
(INSTANCE_COUNT
列) – 实体实例的近似数量。 -
fetchUI
(FETCH_UI
列) – 界面上显示的所获取实体列表的数据量。例如,Filter组件在 Show N rows 字段中使用此数值。
-
maxFetchUI
(MAX_FETCH_UI
列) – 允许获取并传递到客户端的实体实例的最大数量。在某些组件上显示实体列表时会应用此限制,这些组件包括 LookupField、LookupPickerField 以及不带过滤器的表格,表格没有通过
CollectionDatasource.setMaxResults()
方法限制连接的数据源。在这种情况下,数据源本身将获取实例的数量限制为maxFetchUI
。 -
lookupScreenThreshold
(LOOKUP_SCREEN_THRESHOLD
列) – 以实体数量衡量的阈值,确定何时应使用查找界面而不是下拉列表查找实体。选择过滤器参数时,过滤器组件会使用此参数。在达到阈值之前,系统使用LookupField组件,一旦超过阈值,就使用PickerField组件。因此,对于过滤器参数中的特定实体,如果想要使用查找界面,则可以将
lookupScreenThreshold
的值设置为低于instanceCount
的值。
PersistenceManagerMBean
JMX bean 能够通过 DefaultFetchUI
、DefaultMaxFetchUI
、 DefaultLookupScreenThreshold
属性为上面提到的所有参数设置默认值。当实体没有统计信息时,系统将使用相应的默认值,这是一种常见情况。
此外,PersistenceManagerMBean.enterStatistics()
方法允许用户输入实体的统计数据。例如,将以下参数传递给该方法,用来将默认每页记录数设置为 1,000,并将加载到LookupField中最大实例数设置为 30,000:
entityName: sales$Customer
fetchUI: 1000
maxFetchUI: 30000
另一个示例:假设 Customer 实体具有过滤条件,并且希望在条件参数中选择 Customer 时使用查找界面而不是下拉列表。可以使用以下参数调用 enterStatistics()
方法:
entityName: sales$Customer
instanceCount: 2
lookupScreenThreshold: 1
这里忽略了数据库中的实际客户记录数,并手动指定始终超过阈值的数量。
3.10.7. 以 JSON 格式导入和导出实体
平台提供了一个 API,用于以 JSON 格式导出和导入实体关系图。它可以通过 EntityImportExportAPI
接口在中间件使用,也可以通过 EntityImportExportService
在客户端使用。这些接口具有一组相同的方法,如下所述。导出/导入实现委托给 EntitySerializationAPI
接口,也可以直接使用这个接口。
-
exportEntitiesToJSON()
- 将一组实体序列化为 JSON。@Inject private EntityImportExportService entityImportExportService; @Inject private GroupDatasource<Customer, UUID> customersDs; ... String jsonFromDs = entityImportExportService.exportEntitiesToJSON(customersDs.getItems());
-
exportEntitiesToZIP()
- 将一组实体序列化为 JSON,并将 JSON 文件打包为 ZIP 文件。在下面的示例中,使用FileLoader接口将 ZIP 文件保存到文件存储中:@Inject private EntityImportExportService entityImportExportService; @Inject private GroupDatasource<Customer, UUID> customersDs; @Inject private Metadata metadata; @Inject private DataManager dataManager; ... byte[] array = entityImportExportService.exportEntitiesToZIP(customersDs.getItems()); FileDescriptor descriptor = metadata.create(FileDescriptor.class); descriptor.setName("customersDs.zip"); descriptor.setExtension("zip"); descriptor.setSize((long) array.length); descriptor.setCreateDate(new Date()); try { fileLoader.saveStream(descriptor, () -> new ByteArrayInputStream(array)); } catch (FileStorageException e) { throw new RuntimeException(e); } dataManager.commit(descriptor);
-
importEntitiesFromJSON()
- 反序列化 JSON 并根据由entityImportView
参数(参阅 JavaDocs 中的EntityImportView
类)描述的规则持久化反序列后的实体。如果一个实体在数据库中不存在,则会创建该实体。否则,将更新entityImportView
中指定的现有实体的字段。 -
importEntitiesFromZIP()
- 读取包含 JSON 文件的 ZIP 存档,像importEntitiesFromJSON()
方法一样反序列化 JSON 并持久化反序列化的实体。@Inject private EntityImportExportService entityImportExportService; @Inject private FileLoader fileLoader; private FileDescriptor descriptor; ... EntityImportView view = new EntityImportView(Customer.class); view.addLocalProperties(); try { byte[] array = IOUtils.toByteArray(fileLoader.openStream(descriptor)); Collection<Entity> collection = entityImportExportService.importEntitiesFromZIP(array, view); } catch (FileStorageException e) { throw new RuntimeException(e); }
3.10.8. 文件存储
文件存储允许上传、存储和下载与实体相关联的任意文件。在平台标准实现中,使用文件系统中的特定结构将文件存储在主数据库之外。
文件存储机制包括以下部分:
-
FileDescriptor
实体 – 用于表示上传的文件(不要与java.io.FileDescriptor
混淆),使用这个实体就可以通过实体模型对象来引用文件。 -
FileStorageAPI
接口 – 提供对中间层的文件存储的访问。其主要方法有:-
saveStream()
– 根据指定的FileDescriptor
保存文件内容,该文件可以作为InputStream
传递。 -
openStream()
– 根据指定的FileDescriptor
获取文件内容,文件内容以打开的InputStream
的形式返回。
-
-
FileUploadController
类 – Spring MVC 控制器,它使用 HTTP POST 请求将文件从客户端发送到中间件。 -
FileDownloadController
类 – Spring MVC 控制器,它使用 HTTP GET 请求将文件从中间件下载到客户端。 -
FileUpload和FileMultiUpload可视化组件 - 能够将文件从用户的计算机上传到应用程序的客户端层,然后将它们传输到中间件。
-
FileUploadingAPI
接口 – 上传文件到客户端的临时存储。上面提到的可视化组件通过它将文件上传到客户端层。在应用程序代码中可以使用putFileIntoStorage()
方法将文件移动到中间件的永久存储中。 -
FileLoader - 处理文件存储的接口,这个接口在中间层和客户端层都可以使用。
-
ExportDisplay
– 用于将各种应用程序资源下载到用户计算机的客户端层接口。可以使用show()
方法从永久存储中获取文件,该方法需要一个FileDescriptor
参数。可以通过调用AppConfig.createExportDisplay()
静态方法来获得ExportDisplay
的实例,或通过在控制器类中使用注入来获取。
在用户计算机和存储之间的文件双向传输总是通过在输入和输出流之间复制数据来进行。在应用程序的任何层,文件都不会被完全加载到内存中,因此可以传输几乎任何大小的文件。 |
3.10.8.1. 上传文件
通过使用FileUpload和FileMultiUpload组件可以将用户计算机上的文件上传到存储中。本手册中的相应组件说明以及加载和显示图片章节都提供了用法示例。
FileUpload
组件也可在即用型 FileUploadDialog
窗口中使用,这个窗口设计用来加载文件至临时存储。
临时客户端级存储( FileUploadingAPI
)将临时文件存储在由cuba.tempDir应用程序属性定义的文件夹中。如果出现任何故障,临时文件还是保留在文件夹中。cuba_FileUploading
bean 的 clearTempDirectory()
方法由cuba-web-spring.xml文件中定义的定时任务周期性调用来清理临时存储。
FileUploadDialog
对话框提供将文件上传到临时存储的基本功能。窗口包含了上传按钮和投放区域,可以支持从浏览器外拖拽文件至投放区进行上传。
此对话框是使用旧版的界面 API 实现,可以按照下面的方式使用:
@Inject
private Screens screens;
@Inject
private FileUploadingAPI fileUploadingAPI;
@Inject
private DataManager dataManager;
@Subscribe("showUploadDialogBtn")
protected void onShowUploadDialogBtnClick(Button.ClickEvent event) {
FileUploadDialog dialog = (FileUploadDialog) screens.create("fileUploadDialog", OpenMode.DIALOG);
dialog.addCloseWithCommitListener(() -> {
UUID fileId = dialog.getFileId();
String fileName = dialog.getFileName();
File file = fileUploadingAPI.getFile(fileId); (1)
FileDescriptor fileDescriptor = fileUploadingAPI.getFileDescriptor(fileId, fileName); (2)
try {
fileUploadingAPI.putFileIntoStorage(fileId, fileDescriptor); (3)
dataManager.commit(fileDescriptor); (4)
} catch (FileStorageException e) {
throw new RuntimeException(e);
}
});
screens.show(dialog);
}
1 | - 获取 java.io.File 对象,此对象指向文件在 Web 客户端文件系统的位置。一般情况下,文件上传后回放到文件存储中,如果需要处理该文件,则需要获取 File 对象。 |
2 | - 创建一个 FileDescriptor 实体。 |
3 | - 上传文件至中间层的文件存储。 |
4 | - 保存 FileDescriptor 实体。 |
在上传成功后,对话框使用 COMMIT_ACTION_ID
关闭。在 CloseWithCommitListener
监听器中,可以用 getFileId()
和 getFileName()
方法获取上传文件的 UUID 和名称。然后可以获取文件本身或者创建 FileDescriptor
对象并上传文件到文件存储,也可以实现其他的逻辑。
3.10.8.2. 下载文件
可以使用 ExportDisplay
接口将文件从文件存储下载到用户的计算机。可以通过调用 AppConfig.createExportDisplay()
静态方法或通过在控制器类中使用注入来获取 ExportDisplay
实例 。例如:
AppConfig.createExportDisplay(this).show(fileDescriptor);
show()
方法接收一个可选的 ExportFormat
类型的参数,该参数定义内容的类型和文件扩展名。如果没有提供格式,则从 FileDescriptor
中检索扩展名,并将内容类型设置为 application/octet-stream
。
文件扩展名定义文件是通过浏览器标准打开/保存对话框(Content-Disposition = attachment
)下载,还是在浏览器窗口中直接显示(Content-Disposition = inline
)。需要在浏览器窗口中直接显示的文件的扩展名列表由 cuba.web.viewFileExtensions 应用程序属性定义。
如果使用 ByteArrayDataProvider
作为 show()
方法的参数,ExportDisplay
也可以下载任意数据。例如:
public class SampleScreen extends AbstractWindow {
@Inject
private ExportDisplay exportDisplay;
public void onDownloadBtnClick(Component source) {
String html = "<html><head title='Test'></head><body><h1>Test</h1></body></html>";
byte[] bytes;
try {
bytes = html.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
exportDisplay.show(new ByteArrayDataProvider(bytes), "test.html", ExportFormat.HTML);
}
}
3.10.8.3. FileLoader 接口
使用 FileLoader
接口可以在中间层和客户端层使用相同的一组方法处理文件存储。文件的上传和下载是使用“流”的方式执行的:
-
saveStream()
– 将InputStream
内容保存到文件存储中。 -
openStream()
– 返回输入流以从文件存储加载文件内容。
|
作为使用 FileLoader
的一个例子,我们考虑一个简单的任务,将用户输入的内容保存到文本文件中,并在同一界面上的另一个字段中显示文件内容。
该界面包含两个 textArea
字段。假设用户在第一个 textArea
中输入文本,单击下面的 buttonIn
,文本将保存到 FileStorage
。通过单击 buttonOut
,第二个 textArea
将显示保存文件的内容。
下面是上述界面的 XML 描述片段:
<hbox margin="true"
spacing="true">
<vbox spacing="true">
<textArea id="textAreaIn"/>
<button id="buttonIn"
caption="Save text in file"
invoke="onButtonInClick"/>
</vbox>
<vbox spacing="true">
<textArea id="textAreaOut"
editable="false"/>
<button id="buttonOut"
caption="Show the saved text"
invoke="onButtonOutClick"/>
</vbox>
</hbox>
界面控制器包含两个按钮上调用的方法:
-
在
onButtonInClick()
方法中,我们用第一个textArea
的输入内容创建一个字节数组。然后我们创建一个FileDescriptor
对象,并使用其属性定义新文件名、扩展名、大小和创建日期。然后我们使用
FileLoader
的saveStream()
方法保存新文件,将FileDescriptor
传递给它,并使用InputStream
supplier 提供文件内容。最后使用DataManager
接口将FileDescriptor
提交到数据存储。 -
在
onButtonOutClick()
方法中,我们使用FileLoader
的openStream()
方法提取保存的文件的内容。然后我们在第二个textArea
中显示文件的内容。
import com.haulmont.cuba.core.entity.FileDescriptor;
import com.haulmont.cuba.core.global.DataManager;
import com.haulmont.cuba.core.global.FileLoader;
import com.haulmont.cuba.core.global.FileStorageException;
import com.haulmont.cuba.core.global.Metadata;
import com.haulmont.cuba.gui.components.AbstractWindow;
import com.haulmont.cuba.gui.components.ResizableTextArea;
import com.haulmont.cuba.gui.upload.FileUploadingAPI;
import org.apache.commons.io.IOUtils;
import javax.inject.Inject;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
public class FileLoaderScreen extends AbstractWindow {
@Inject
private Metadata metadata;
@Inject
private FileLoader fileLoader;
@Inject
private DataManager dataManager;
@Inject
private ResizableTextArea textAreaIn;
@Inject
private ResizableTextArea textAreaOut;
private FileDescriptor fileDescriptor;
public void onButtonInClick() {
byte[] bytes = textAreaIn.getRawValue().getBytes();
fileDescriptor = metadata.create(FileDescriptor.class);
fileDescriptor.setName("Input.txt");
fileDescriptor.setExtension("txt");
fileDescriptor.setSize((long) bytes.length);
fileDescriptor.setCreateDate(new Date());
try {
fileLoader.saveStream(fileDescriptor, () -> new ByteArrayInputStream(bytes));
} catch (FileStorageException e) {
throw new RuntimeException(e);
}
dataManager.commit(fileDescriptor);
}
public void onButtonOutClick() {
try {
InputStream inputStream = fileLoader.openStream(fileDescriptor);
textAreaOut.setValue(IOUtils.toString(inputStream));
} catch (FileStorageException | IOException e) {
throw new RuntimeException(e);
}
}
}
3.10.8.4. 标准文件存储实现
文件存储的标准实现以特定的文件夹结构将文件存储在一个或多个位置。
特定文件夹结构的根目录可以在cuba.fileStorageDir应用程序属性中定义,格式是以逗号分隔的路径列表。例如:
cuba.fileStorageDir=/work/sales/filestorage,/mnt/backup/filestorage
如果未定义该属性,则存储将位于中间件工作目录的 filestorage
子文件夹中。该文件夹在标准 Tomcat 部署中是 tomcat/work/app-core/filestorage
。
如果定义了多个存储位置,存储的行为如下:
-
列表中的第一个文件夹被视为 主(primary),其它文件夹被视为 备份(backup)。
-
存储的文件首先放在主文件夹中,然后复制到所有的备份目录中。
在存储文件之前,系统会检查每个文件夹是否可访问。如果无法访问主目录,系统将抛出异常而不存储文件。如果有备份目录无法访问,则文件将存储在一个可用的目录中,并记录相应的错误。
-
从主目录中读取文件。
如果无法访问主目录,系统将从包含所需文件的第一个可用备份目录中读取文件,并记录相应的错误。
存储文件夹结构按以下方式组织:
-
子目录有三层,代表文件上传日期 – 年 、月 、 日。
-
文件实际保存在
日
目录中。文件名与相应的FileDescriptor
对象的标识符一致。文件扩展名与源文件的扩展名一致。 -
文件夹结构的根文件夹包含一个
storage.log
文件,其中包含每个存储文件的信息,包括用户和上传时间。存储机制本身不需要此日志,但这个日志可用于故障排除。
app-core.cuba:type=FileStorage
JMX bean 显示当前的存储根目录集合,并提供以下排除故障的方法:
-
findOrphanDescriptors()
– 查找数据库中有,而存储中没有对应文件的记录。 -
findOrphanFiles()
– 查找存储中有,而数据库中没相应记录的文件。
3.10.8.5. Amazon S3 文件存储实现
标准文件存储实现可以使用云存储服务代替。建议为云部署使用单独的云文件存储服务,因为部署应用的云服务器通常不能保证外部文件在其硬盘上的永久存储。
平台提供了开箱即用的 Amazon S3 文件存储服务支持。如果要支持其它服务就需要实现自定义逻辑。
要添加 Amazon S3 支持,首先,需要在 core 模块的 spring.xml
文件中注册 AmazonS3FileStorage
类:
<bean name="cuba_FileStorage"
class="com.haulmont.cuba.core.app.filestorage.amazon.AmazonS3FileStorage"/>
接下来,应该在 core 模块的 app.properties
文件中定义 Amazon 设置:
cuba.amazonS3.accessKey = <Access Key>
cuba.amazonS3.secretAccessKey = <Secret Access Key>
cuba.amazonS3.region = <Region>
cuba.amazonS3.bucket = <Bucket Name>
|
存储文件夹结构的组织方式与标准实现类似。
3.10.9. 文件夹面板
文件夹面板提供对常用信息的快速访问。它是主应用程序窗口左侧的一个包含多级文件夹的面板。点击文件夹会使用特定参数打开相应的系统界面。
在撰写本文时,该面板仅适用于 Web Client。
平台支持三种类型的文件夹:应用程序文件夹 、 搜索文件夹 和 记录集。应用程序文件夹作为单独的文件夹树显示在面板顶部;搜索文件夹和记录集显示在面板底部的组合树中。如果设置cuba.web.foldersPaneEnabled属性为 true
,文件夹将支持键盘快捷键。
通过下列应用程序属性可以定制文件夹面板的功能:
3.10.9.1. 应用程序文件夹
创建或编辑应用程序文件夹需要特殊的权限(cuba.gui.appFolder.global
)。
可以通过文件夹面板右键菜单创建一个简单的应用程序文件夹。这类文件夹不会链接到系统界面,只用于对文件夹树中的其它文件夹进行分组。
打开带有过滤器界面的文件夹可以按以下方式创建:
-
打开界面并根据需要过滤记录。
-
在 Filter… 按钮菜单中选择 Save as application folder 选项。
-
在 Add 对话框中填写文件夹属性:
-
Folder name。
-
Screen Caption – 从文件夹中打开窗口时要添加到窗口标题的字符串。
-
Parent folder – 确定新文件夹在文件夹树中的位置。
-
Visibility script – 确定文件夹可见性的 Groovy 脚本,在用户会话建立时执行。
该脚本应该返回一个
Boolean
值。如果未定义脚本或脚本执行结果为true
或者null
,则文件夹可见。Groovy 脚本的示例:userSession.currentOrSubstitutedUser.login == 'admin'
-
Quantity script – 一个用于定义文件夹上显示的记录数和样式的 Groovy 脚本。在用户会话建立时、计器调用时执行。
该脚本会返回一个数值,其整数部分将用作记录数。如果未定义脚本或脚本执行返回
null
,则不会显示记录数。除了返回值之外,该脚本还可以设置style
变量,该变量将用作文件夹显示样式。Groovy 脚本的示例:def em = persistence.getEntityManager() def q = em.createQuery('select count(o) from sales$Order o') def count = q.getSingleResult() style = count > 0 ? 'emphasized' : null return count
要显示样式,应该在程序主题中定义
cuba-folders-pane
的v-tree-node
元素样式,例如:.c-folders-pane .v-tree-node.emphasized { font-weight: bold; }
-
Order No – 文件夹的在树中的顺序。
-
脚本可以使用 groovy.lang.Binding
上下文中定义的以下变量:
-
folder
– 执行脚本的AppFolder
实体的实例。 -
persistence
– Persistence接口的实现。 -
metadata
– Metadata接口的实现。
在更新文件夹时,平台对所有脚本使用相同的 groovy.lang.Binding
实例。因此,可以在它们之间传递变量,这样可以消除重复的请求并能提高性能。
脚本源代码可以存储在 AppFolder
实体的属性中,也可以存储在单独的文件中。如果要存储在单独文件中,属性值设置为扩展名为 ".groovy" 的文件路径,这是Resources接口需要的。如果属性值是以".groovy"结尾的字符串,则将从相应的文件加载脚本;否则,将属性内容本身将用作脚本。
应用程序文件夹是 AppFolder
实体的实例,存储在相关的 SYS_FOLDER 表和 SYS_APP_FOLDER 表中。
3.10.9.2. 搜索文件夹
用户可以创建与应用程序文件夹类似的搜索文件夹。分组文件夹可以直接通过文件夹面板的右键菜单创建。链接到界面的文件夹可以使用 Filter… 按钮菜单上的“Save as search folder”选项创建。
要创建全局搜索文件夹,要求用户具有 Create/edit global search folders 权限(cuba.gui.searchFolder.global
)。
可以在创建文件夹后再次编辑搜索文件夹的过滤器:打开文件夹并更改 Folder:{folder name} 过滤器,保存过滤器的同时会更改文件夹过滤器。
搜索文件夹是 SearchFolder
实体的实例,存储在相关的 SYS_FOLDER 和 SEC_SEARCH_FOLDER 表。
3.10.9.3. 记录集
<layout>
<filter id="customerFilter"
datasource="customersDs"
applyTo="customersTable"/>
<groupTable id="customersTable"
width="100%">
<buttonsPanel>
<button action="customersTable.create"/>
...
</buttonsPanel>
...
Add to set 或 Add to current set / Remove from set 会出现在表格右键菜单中。如果一个表格包含 buttonsPanel
(如上例所示),则还会添加相应的表格按钮。
记录集是 SearchFolder
实体的实例,存储在相关的 SYS_FOLDER 和 SEC_SEARCH_FOLDER 表。
3.10.10. 关于软件组件信息
平台提供了注册应用程序中使用的第三方软件组件信息(credits)以及在 UI 中显示这些组件信息的功能。组件信息包括软件组件名称、网站链接和许可文本。
平台提供的应用程序组件包含了自己的描述文件,如 com/haulmont/cuba/credits.xml
、 com/haulmont/reports/credits.xml
等。cuba.creditsConfig应用程序属性可用于指定应用程序的描述文件。
credits.xml
文件的结构如下:
-
items
元素列出了使用的库及其许可文本,许可文本既可以在嵌入的license
元素中声明,也可以在license
属性中指定一个指向licenses
文本的链接。可以引用在当前文件中声明的许可,也可以引用在
cuba.creditsConfig
变量中声明的任何其它文件中的许可,前提是这些文件在当前文件之前。 -
licenses
元素列出了用到的通用许可的文本(例如 LGPL)。
可以使用 com/haulmont/cuba/gui/app/core/credits/credits-frame.xml
框架(frame)显示所有第三方软件组件列表,该框架从 cuba. creditsConfig
中定义的文件加载信息。下面是在界面中嵌入这个框架的示例:
<dialogMode width="500" height="400"/>
<layout expand="creditsBox">
<groupBox id="creditsBox"
caption="msg://credits"
width="100%">
<frame id="credits"
src="/com/haulmont/cuba/gui/app/core/credits/credits-frame.xml"
width="100%"
height="100%"/>
</groupBox>
</layout>
如果以对话框模式(WindowManager.OpenType.DIALOG
)打开包含该框架的界面,则必须指定高度;否则,滚动条可能无法正常工作。请参阅上面示例中的 dialogMode
元素。
3.10.11. MyBatis 集成
MyBatis 框架,与 ORM 本地查询或QueryRunner相比,提供了更广泛的执行 SQL 和将查询结果映射到对象的功能。
按照下面的步骤在 CUBA 项目中集成 MyBatis。
-
在 core 模块的根 java 包创建处理 UUID 类型的类。
import com.haulmont.cuba.core.global.UuidProvider; import org.apache.ibatis.type.JdbcType; import org.apache.ibatis.type.TypeHandler; import java.sql.*; public class UUIDTypeHandler implements TypeHandler { @Override public void setParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException { ps.setObject(i, parameter, Types.OTHER); } @Override public Object getResult(ResultSet rs, String columnName) throws SQLException { String val = rs.getString(columnName); if (val != null) { return UuidProvider.fromString(val); } else { return null; } } @Override public Object getResult(ResultSet rs, int columnIndex) throws SQLException { String val = rs.getString(columnIndex); if (val != null) { return UuidProvider.fromString(val); } else { return null; } } @Override public Object getResult(CallableStatement cs, int columnIndex) throws SQLException { String val = cs.getString(columnIndex); if (val != null) { return UuidProvider.fromString(val); } else { return null; } } }
-
在 core 模块的 spring.xml 文件所在目录创建
mybatis.xml
配置文件,在文件内正确引用UUIDTypeHandler
:<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <settings> <setting name="lazyLoadingEnabled" value="false"/> </settings> <typeHandlers> <typeHandler javaType="java.util.UUID" handler="com.company.demo.core.UUIDTypeHandler"/> </typeHandlers> </configuration>
-
将下面的 bean 都添加到
spring.xml
文件以便在项目中使用 MyBatis:<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="cubaDataSource"/> <property name="configLocation" value="com/company/demo/mybatis.xml"/> <property name="mapperLocations" value="com/company/demo/core/sqlmap/*.xml"/> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com/company/demo.core.dao"/> <property name="sqlSessionFactory" ref="sqlSessionFactory"/> </bean> <bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate"> <constructor-arg index="0" ref="sqlSessionFactory" /> </bean>
sqlSessionFactory
bean 包含了指向mybatis.xml
的引用。MapperLocations
参数定义了mapperLocations
映射文件的路径(根据 Spring 中ResourceLoader
接口的资源解析规则)。 -
最后,在 build.gradle 中的
core
模块添加 MyBatis 的依赖:compile('org.mybatis:mybatis:3.2.8') compile('org.mybatis:mybatis-spring:1.2.5')
下面是一个映射文件的示例,用于加载 订单(Order)
的实例以及相关的 客户(Customer)
和 订单商品(order item)
集合:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.sample.sales">
<select id="selectOrder" resultMap="orderResultMap">
select
o.ID as order_id,
o.DATE as order_date,
o.AMOUNT as order_amount,
c.ID as customer_id,
c.NAME as customer_name,
c.EMAIL as customer_email,
i.ID as item_id,
i.QUANTITY as item_quantity,
p.ID as product_id,
p.NAME as product_name
from
SALES_ORDER o
left join SALES_CUSTOMER c on c.ID = o.CUSTOMER_ID
left join SALES_ITEM i on i.ORDER_ID = o.id and i.DELETE_TS is null
left join SALES_PRODUCT p on p.ID = i.PRODUCT_ID
where
c.id = #{id}
</select>
<resultMap id="orderResultMap" type="com.sample.sales.entity.Order">
<id property="id" column="order_id"/>
<result property="date" column="order_date"/>
<result property="amount" column="order_amount"/>
<association property="customer" column="customer_id" javaType="com.sample.sales.entity.Customer">
<id property="id" column="customer_id"/>
<result property="name" column="customer_name"/>
<result property="email" column="customer_email"/>
</association>
<collection property="items" ofType="com.sample.sales.entity.Item">
<id property="id" column="item_id"/>
<result property="quantity" column="item_quantity"/>
<association property="product" column="product_id" javaType="com.sample.sales.entity.Product">
<id property="id" column="product_id"/>
<result property="name" column="product_name"/>
</association>
</collection>
</resultMap>
</mapper>
以下代码可用于获取上面示例中的查询结果:
try (Transaction tx = persistence.createTransaction()) {
SqlSession sqlSession = AppBeans.get("sqlSession");
Order order = (Order) sqlSession.selectOne("com.sample.sales.selectOrder", orderId);
tx.commit();
3.10.12. 悲观锁
当同时编辑单个实体实例的机率很高的情况下,应使用悲观锁。在这种情况下,基于实体版本控制的标准乐观锁通常会产生很多冲突。
悲观锁在编辑界面中打开实体实例时显式地锁定实体实例。这样,在同一时刻只有一个用户可以编辑这个实体实例。
悲观锁机制也可用于管理其它任何任务的并发处理,它提供的关键的好处在于锁是分布式的,这是因为锁会在中间件集群中进行复制。JavaDocs 中提供了更多有关 LockManagerAPI
和 LockService
接口的详细信息。
可以使用 Administration > Locks > Setup 界面在应用程序开发或生产环境为任何实体类启用悲观锁,或者进行如下操作:
-
向 SYS_LOCK_CONFIG 表中插入一条包含以下字段值的新记录:
-
ID – 任意 UUID 类型的标识符。
-
NAME – 要锁定的对象的名称。对于实体,应该是其元类的名称。
-
TIMEOUT_SEC – 以秒为单位的锁定超时时间。
例如:
insert into sys_lock_config (id, create_ts, name, timeout_sec) values (newid(), current_timestamp, 'sales$Order', 300)
-
-
重启应用程序服务或调用
app-core.cuba:type=LockManager
JMX bean 的reloadConfiguration()
方法。
可以通过 app-core.cuba:type=LockManager
JMX bean 或通过 Administration > Locks 界面跟踪当前的锁状态。此界面还可以对任何对象进行解锁。
3.10.13. 使用 QueryRunner 执行 SQL
QueryRunner
是一个用于执行 SQL 的类。在以下情况应该使用它来代替 JDBC:必须使用纯 SQL,并且不需要使用具有相同功能的ORM 工具。
平台的 QueryRunner 是 Apache DbUtils QueryRunner 的一种变体,增加了对 Java 泛型的支持。
用法示例:
QueryRunner runner = new QueryRunner(persistence.getDataSource());
try {
Set<String> scripts = runner.query("select SCRIPT_NAME from SYS_DB_CHANGELOG",
new ResultSetHandler<Set<String>>() {
public Set<String> handle(ResultSet rs) throws SQLException {
Set<String> rows = new HashSet<String>();
while (rs.next()) {
rows.add(rs.getString(1));
}
return rows;
}
});
return scripts;
} catch (SQLException e) {
throw new RuntimeException(e);
}
有两种使用 QueryRunner
的方法:在当前事务或自动提交模式的单独事务中使用。
-
要在当前事务中使用
QueryRunner
查询必须使用无参数构造函数创建QueryRunner
的实例 。然后,应该使用EntityManager.getConnection()
返回的Connection
作为参数来调用query()
或update()
方法。在查询之后不需要关闭Connection
,因为连接会在提交事务时关闭。 -
要在单独的事务中运行查询,必须调用带参数的构造函数创建
QueryRunner
实例,该构造函数使用Persistence.getDataSource()
方法返回的DataSource
作为参数。然后,调用query()
或update()
方法,不需要Connection
参数。这时将从指定的DataSource
创建连接,查询完成后这个连接会立即关闭。
3.10.14. 计划任务的执行
平台提供了两种运行计划任务的方法:
-
Spring Framework 中标准的
TaskScheduler
机制。 -
使用平台自身的计划任务执行机制。
3.10.14.1. Spring 任务调度
Spring Framework 手册的 Task Execution and Scheduling 部分详细描述了这种机制。
TaskScheduler
可用于在中间层和客户端层的任何应用程序块(block)中运行任意的 Spring bean 对象方法。
spring.xml中的配置示例:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:task="http://www.springframework.org/schema/task"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-4.3.xsd">
<!--...-->
<task:scheduled-tasks scheduler="scheduler">
<task:scheduled ref="sales_Processor" method="someMethod" fixed-rate="60000"/>
<task:scheduled ref="sales_Processor" method="someOtherMethod" cron="0 0 1 * * MON-FRI"/>
</task:scheduled-tasks>
</beans>
在上面的例子中,声明了两个任务,它们调用 sales_Processor
bean 的 someMethod()
方法和 someOtherMethod()
方法。从应用程序启动时起,将以固定的时间间隔(60 秒)调用 someMethod()
方法。根据 Cron 表达式定义的时间表调用 someOtherMethod()
方法(有关此表达式格式的描述,请参阅 http://quartz-scheduler.org/documentation/quartz-1.x/tutorials/crontrigger )。
任务的实际启动由 scheduled-tasks
元素的 scheduler
属性中指定的 bean 执行。它是 CubaThreadPoolTaskScheduler
类型的 bean,在 cuba 应用程序组件的 core 和 web 模块中配置(参阅 cuba-spring.xml
、 cuba-web-spring.xml
)。该类提供处理一些 CUBA 框架内特定任务的功能。
如果要向中间层的 Spring 定时任务执行的代码提供SecurityContext,请使用系统身份验证。
3.10.14.2. CUBA 计划任务
CUBA 计划任务机制用于有计划地执行中间件中任意 Spring bean 方法。此机制的目标与上述标准 Spring Framework 调度程序的区别在于:
-
能够在应用程序运行过程中时配置任务而无需重启服务。
-
在中间件集群中协调单例任务的执行,提供以下功能:
-
可有效地防止任务同时执行。
-
按优先级将任务绑定到服务。
-
单例(singleton) 任务指在同一时刻只允许在一个服务上执行的任务。例如,从队列中读取并发送电子邮件的任务。
3.10.14.2.1. 任务注册
任务在数据库表 SYS_SCHEDULED_TASK
中注册,该表对应于 ScheduledTask
实体。平台提供了用于管理任务的浏览和编辑界面: Administration → Scheduled Tasks 。
下面是关于任务属性的描述:
-
Defined by – 描述哪个软件对象实现该任务。可能的值是:
-
Bean – 该任务由 Spring托管 bean的方法实现。额外属性:
-
Bean name – 托管 bean 的名称。
只有在 core 模块中定义并且具有接口(该接口包含适合任务调用的方法)时,才会列出该 bean 并且可供选择。不支持没有接口的 bean。
-
Method name – 执行的 bean 接口方法。该方法要么没有参数,要么所有参数必须是
String
类型。方法的返回值类型可以是void
或String
。后一种情况的返回值将存储在执行表中(参阅下面的 Log finish )。 -
Method parameters – 所选方法的参数。仅支持
String
类型的参数。
-
-
Class – 该任务是一个实现
java.util.concurrent.Callable
接口的类。该类必须具有 public 修饰符的无参构造函数。附加属性:-
Class name – 类名。
-
-
Script – 该任务是一个 Groovy 脚本。该脚本由Scripting.runGroovyScript()执行。附加属性:
-
Script name – 脚本名。
-
-
-
User name – 一个用户名,以其身份执行任务。如果未指定,则以cuba.jmxUserLogin应用程序属性中指定的用户的身份来执行该任务。
-
Singleton – 表示该任务是单例,即应该只在一个应用程序服务上运行。
-
Scheduling type – 任务调度的方式:
-
Cron – Cron 表达式是由六个字段组成的序列,用空格分隔:秒、分钟、小时、日、月、星期。月份和星期可以用英文名称的前三个字母表示。例如:
-
0 0 * * * * – 每天每小时的开始时刻
-
*/10 * * * * * – 每 10 秒钟
-
0 0 8-10 * * * – 每天 8 点、9 点和 10 点
-
0 0/30 8-10 * * * – 每天 8:00、 8:30、 9:00、 9:30 和 10 点
-
0 0 9-17 * * MON-FRI – 工作日每天 9 点到 17 点的整点时刻
-
0 0 0 25 DEC ? – 每个圣诞节的午夜 12 点
-
-
Period – 以秒为单位周期性执行
-
Fixed Delay – 完成前一次执行后,将延迟在 Period 中指定的时间之后再次执行任务。
-
-
Period – 如果 Scheduling type 是 Period 或 Fixed Delay,任务将以秒为单位周期性重复执行或延迟固定时间后执行。
-
Timeout – 以秒为单位的时间,到期时无论是否存在关于任务完成的信息,都认为任务已执行完成。如果未明确设置超时时间,则假定为 3 小时。
-
Start date – 首次执行的日期或时间。如果未指定,则在服务启动时立即执行该任务。如果指定,则在
startDate + period * N
启动执行任务,其中 N 是整数。对于周期任务来说,N 可以是大于 1 的数,对延迟任务来说 N 是 1。只为偶发任务指定
Start date
是合理的,即每小时运行一次、每天运行一次等。 -
Time frame – 如果指定了
Start date
,Time frame
定义了以秒为单位的时间窗口,任务将在startDate + period * N
时间到期后的Time frame
秒内启动。如果没有明确指定Time frame
,则它等于period / 2
。如果未指定
Start date
,则忽略Time frame
,即任务将在上一次任务到期执行后的Period
之后的任何时间启动。 -
Start delay - 服务启动并激活调度后,延迟执行的秒数。如果任务会拖慢服务启动的速度,可考虑为任务设置此参数。
-
Permitted servers – 以逗号分隔的具有运行此任务权限的服务器标识符列表,如果未指定列表,则可以在任何服务器上执行该任务。
对于单例任务,列表中服务器的顺序决定了执行优先级:第一个服务器的优先级高于最后一个。具有较高优先级的服务器将拦截单例的执行,如下所示:如果具有较高优先级的服务器检测到该任务先前已由具有较低优先级的服务器执行,则无论
Period
是否过期,它都会启动该任务。服务器优先级仅在 Scheduling type 为
Period
并且未指定 Start date 属性时有效。否则,服务会同时开始,也就没有机会进行拦截了。 -
Log start – 标记任务启动是否应该在
SYS_SCHEDULED_EXECUTION
表中记录,该表对应于ScheduledExecution
实体。在目前的实现中,如果任务是单例,无论此标志是什么都会记录启动状态。
-
Log finish – 标记任务完成是否应该在
SYS_SCHEDULED_EXECUTION
表中记录,该表对应于ScheduledExecution
实体。在目前的实现中,如果任务是单例,则无论此标志是什么都会记录完成状态。
-
Description – 任务的文本描述。
任务也具有激活标志,可以在任务列表界面中设置。非激活任务会被忽略。
3.10.14.2.2. 任务处理控制
-
为了启用任务处理,cuba.schedulingActive应用程序属性应设置为
true
。可以在 Administration > Application Properties 界面中执行此操作,也可以通过app-core.cuba:type=Scheduling
JMX bean(请参阅它的Active
属性)执行此操作。 -
一切通过系统界面对任务所做的更改将立即对集群中的所有服务器生效。
-
app-core.cuba:type=Scheduling
JMX bean 的removeExecutionHistory()
方法可用于删除旧的执行历史记录。该方法有两个参数:-
age
– 任务执行后经过的时间(以小时为单位)。 -
maxPeriod
– 应删除的任务执行历史记录的最大Period
(以小时为单位)。这样可以仅删除频繁执行的任务的历史记录,同时保留每天一次执行历史记录。该方法可以自动调用。使用以下参数创建新任务:
-
Bean name –
cuba_SchedulingMBean
-
Method name –
removeExecutionHistory(String age, String maxPeriod)
-
Method parameters – 例如,
age
= 72,maxPeriod
= 12。
-
-
3.10.14.2.3. 调度实现细节
-
任务处理(
SchedulingAPI.processScheduledTasks()
方法)的调用时间间隔在cuba-spring.xml
中设定,默认为 1 秒。它设置了调用任务之间的最小间隔,该间隔应该高于两倍,即 2 秒。建议不要降低这些值。 -
目前定时任务调度的实现是使用数据库表中的行级锁实现的同步。这表示在高负载下,数据库可能无法及时响应调度程序,并且可能需要增加启动间隔(大于 1 秒),因此启动任务的最小周期将相应增加。
-
如果未设定
允许的服务器(Permitted servers)
属性,单例任务仅在群集中的主节点上执行(如果满足其它条件)。需要注意的是群集外的独立服务器也被视为主服务器。 -
如果先前执行的任务尚未完成且指定的
Timeout
尚未到期,则不会启动该任务。对于当前实现中的单例任务,这是使用数据库中的信息实现的;对于非单例任务,执行状态表保存在服务器内存中。 -
此执行机制为用户创建并缓存用户会话,可以在任务的 User name 属性中指定,或者在cuba.jmxUserLogin应用程序属性中指定。会话信息可以通过标准的UserSessionSource接口在已启动任务的执行线程中获得。
正确执行单例任务需要中间件服务器精确的时间同步! |
3.10.15. 界面链接
参阅 URL 历史及导航 部分,这里描述了 URL 和应用程序界面映射的更多功能。 |
Web 客户端block允许通过 URL 中提供的命令打开应用程序界面。如果浏览器没有已登录用户的会话信息,则应用程序将首先显示登录界面,在身份验证成功后进入应用程序主窗口,同时打开请求的界面。
支持的命令列表由cuba.web.linkHandlerActions应用程序属性定义。默认情况下是 open
和 o
。在处理 HTTP 请求时,将分析 URL 的最后一部分,如果它与已注册的命令匹配,则将控制权传递给适当的处理器,该处理器是实现 LinkHandlerProcessor
接口的 bean。
平台提供了一个接受以下请求参数的处理器:
-
screen
– screens.xml中定义的界面名称,例如:http://localhost:8080/app/open?screen=sec$User.browse
-
item
– 要传递给编辑界面的实体实例,根据EntityLoadInfo
类的约定进行编码,即entityName-instanceId
或entityName-instanceId-viewName
。例如:http://localhost:8080/app/open?screen=sec$User.edit&item=sec$User-60885987-1b61-4247-94c7-dff348347f93 http://localhost:8080/app/open?screen=sec$User.edit&item=sec$User-60885987-1b61-4247-94c7-dff348347f93-user.edit
要在打开的编辑器界面中直接创建新的实体实例,请在实体类名称前添加
NEW-
前缀,例如:http://localhost:8080/app/open?screen=sec$User.edit&item=NEW-sec$User
-
params
– 界面控制器的init()
方法的参数。参数格式为name1:value1,name2:value2
。参数值可以包括根据EntityLoadInfo
类的约定编码的实体实例。例如:http://localhost:8080/app/open?screen=sales$Customer.lookup¶ms=p1:v1,p2:v2 http://localhost:8080/app/open?screen=sales$Customer.lookup¶ms=p1:sales$Customer-01e37691-1a9b-11de-b900-da881aea47a6
如果要提供其它 URL 命令,请执行以下操作:
-
在项目的 web 模块中创建一个实现了
LinkHandlerProcessor
接口的bean。 -
如果应当由新的 bean 处理当前的 URL(URL 参数存储在
ExternalLinkContext
对象),那么这个 bean 的canHandle()
方法必须返回 true。 -
在
handle()
方法中执行请求的操作。
bean 可以选择实现 Spring 的 Ordered
接口或包含 Order
注解。这样,可以在处理器链中指定 bean 的顺序。使用 LinkHandlerProcessor
接口的 HIGHEST_PLATFORM_PRECEDENCE
和 LOWEST_PLATFORM_PRECEDENCE
常量将 bean 放在平台中定义的处理器之前或之后。因此,如果指定的顺序小于 HIGHEST_PLATFORM_PRECEDENCE
,则会更早地请求 bean,并且可以根据需要覆盖平台处理器定义的操作。
3.10.16. 序列生成
该机制可以通过单个 API 生成唯一的数字序列,并且与 DBMS 类型无关。
-
getNextNumber(String domain)
– 获取序列的下一个值。该机制能够同时管理由任意字符串标识的多个序列。需要获取值的序列的名称通过domain
参数传递。序列不需要初始化。当第一次调用
getNextNumber()
方法时,将创建相应的序列并返回 1。 -
getCurrentNumber(String domain)
– 获得当前值,也就是序列生成的最后一个值。domain
参数设置序列名称。 -
setCurrentNumber(String domain)
– 设置序列的当前值。下一次调用getNextNumber()
方法时将返回这个值递增 1 之后的值。
下面是在一个中间件 bean 对象中获取序列中的下一个值的示例:
@Inject
private UniqueNumbersAPI uniqueNumbers;
private long getNextValue() {
return uniqueNumbers.getNextNumber("mySequence");
}
服务 的 getNextNumber()
方法用于在客户端 block 中获取序列值。
与 UniqueNumbersAPI
拥有相同方法的 app-core.cuba:type=UniqueNumbers
JMX bean用于管理序列。
序列生成机制的实现取决于 DBMS 类型。序列参数也可以直接在数据库中管理,但方式不同。
-
对于 HSQL、 Microsoft SQL Server 2012+ 、 PostgreSQL 和 Oracle,每个
UniqueNumbersAPI
序列对应于数据库中名称为SEC_UN_{domain}
格式的数据库原生序列。 -
对于 2012 之前的 Microsoft SQL Server,每个序列对应一个带有 IDENTITY 类型主键的
SEC_UN_{domain}
表。 -
对于 MySQL,序列对应于
SYS_SEQUENCE
表中的记录。
3.10.17. 用户会话日志
这个机制用于使系统管理员可以获得用户登录和注销的历史数据。日志记录机制基于跟踪用户会话。每次创建 UserSession
对象时,日志记录都会将以下字段信息保存到数据库中:
-
用户会话 ID。
-
用户 ID。
-
代替用户 ID。
-
用户的最后一次动作(登录/注销/超时/终止)。
-
请求登录的远程 IP 地址。
-
用户会话的客户端类型(网页、门户)。
-
服务器 ID(例如,
localhost:8080/app-core
)。 -
事件开始日期。
-
事件结束日期。
-
客户端信息(会话环境:操作系统、Web 浏览器等)。
默认情况下,不启用用户会话日志记录机制。启用日志记录机制的最简单方法是使用 Administration > User Session Log 应用界面上的 Enable Logging 按钮。或者使用 cuba.UserSessionLogEnabled
应用属性。
如果需要的话可以创建 sec$SessionLogEntry
实体的报表。
3.11. 功能扩展
平台可以在应用程序中扩展和覆盖其以下方面的功能:
-
扩展实体属性集。
-
扩展界面功能。
-
扩展和覆盖 Spring bean 中包含的业务逻辑。
下面是前两个操作的示例,通过将 "Address" 字段添加到平台安全子系统的 User
实体来说明。
3.11.1. 扩展实体
在应用程序项目中,从 com.haulmont.cuba.security.entity.User
中派生一个实体类,添加需要的属性和相应的访问方法:
@Entity(name = "sales$ExtUser")
@Extends(User.class)
public class ExtUser extends User {
@Column(name = "ADDRESS", length = 100)
private String address;
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
应使用@Entity注解指定实体的新名称。由于父实体没有定义继承策略,因此使用的是默认继承策略 SINGLE_TABLE
。这意味着子实体将与父实体存储在同一个表中,并且不需要给子实体添加@Table注解。其它父实体注解(@NamePattern、 @Listeners等)会自动应用于子实体,并且可以在子实体中覆盖。
新实体类的一个重要元素是 @Extends
注解,它需要一个父类作为参数。它可以创建一个子实体的注册表,并强制平台机制在所有地方使用子实体来代替父实体。注册表由 ExtendedEntities
类实现,该类是一个名为 cuba_ExtendedEntities
的 Spring bean,也可以通过元数据接口访问。
将新属性的本地化名称添加到 com.sample.sales.entity
消息包:
messages.properties
ExtUser.address=Address
messages_ru.properties
ExtUser.address=Адрес
在项目的persistence.xml文件中注册新实体:
<class>com.sample.sales.entity.ExtUser</class>
将相应表的更新脚本添加到数据库创建和更新脚本:
-- add column for "address" attribute
alter table SEC_USER add column ADDRESS varchar(100)
^
-- add discriminator column required for entity inheritance
alter table SEC_USER add column DTYPE varchar(100)
^
-- set discriminator value for existing records
update SEC_USER set DTYPE = 'sales$ExtUser' where DTYPE is null
^
要在界面中使用新的实体属性,请为新实体创建视图,新视图的名称与基础实体的名称保持一致。新视图应继承基础视图并定义新属性,例如:
<view class="com.sample.sales.entity.ExtUser"
name="user.browse"
extends="user.browse">
<property name="address"/>
</view>
继承的视图将包含其父视图中的所有属性。如果基础视图继承 _local
视图,并且在新视图中只添加本地属性,则不需要继承视图,在这种情况下,可以省略该步骤。
3.11.2. 扩展界面
平台支持通过继承现有的界面描述来创建新的界面 XML 描述。
通过在 window
根元素的 extends
属性中指定父描述路径来实现 XML 继承。
XML 界面元素覆盖规则:
-
如果新扩展的界面描述中有某个元素,则将使用以下算法在父描述中搜索相应的元素:
-
如果覆盖的元素有
id
属性,则会在父描述中搜索具有相同id
的相应元素。 -
如果搜索成功,则找到的元素被 覆盖。
-
否则,平台将先确定父描述中包含具有提供的路径和名称的元素数量。如果只有一个元素,则它被 覆盖。
-
如果搜索没有产生结果,并且在父描述中没有具有给定路径和名称的元素或者有多个这种元素,则会 添加 新元素。
-
-
被覆盖或添加的元素的文本将从扩展的新元素中复制。
-
扩展的新元素的所有属性都会复制到被覆盖或添加的元素中。如果属性名称匹配,则从扩展的新元素中获取值。
-
默认情况下,新元素将添加到元素列表的末尾。要将新元素添加到开头或使用任意位置,可以执行以下操作:
-
在继承描述中定义一个额外的命名空间:
xmlns:ext="http://schemas.haulmont.com/cuba/window-ext.xsd"
。 -
添加带有所需索引的
ext:index
属性到扩展元素,例如:ext:index="0"
。
-
要调试 XML 描述的转换过程,可以通过在 Logback 配置文件中将 com.haulmont.cuba.gui.xml.XmlInheritanceProcessor
记录器指定为 TRACE
级别,从而将结果 XML 输出到服务端日志。
- 扩展历史版本界面
-
首先,看看
ExtUser
实体的浏览界面:ext-user-browse.xml<?xml version="1.0" encoding="UTF-8" standalone="no"?> <window xmlns="http://schemas.haulmont.com/cuba/window.xsd" xmlns:ext="http://schemas.haulmont.com/cuba/window-ext.xsd" extends="/com/haulmont/cuba/gui/app/security/user/browse/user-browse.xml"> <layout> <groupTable id="usersTable"> <columns> <column ext:index="2" id="address"/> </columns> </groupTable> </layout> </window>
在此示例中,XML 描述继承自框架的标准
User
实体浏览界面。address
列以2
为索引被添加到表中,因此它在login
和name
列之后显示。如果在screens.xml中注册一个新界面,新界面使用与父界面相同的标识符,这样新界面就会代替旧界面。
<screen id="sec$User.browse" template="com/sample/sales/gui/extuser/extuser-browse.xml"/> <screen id="sec$User.lookup" template="com/sample/sales/gui/extuser/extuser-browse.xml"/>
同样,创建一个编辑界面:
ext-user-edit.xml<?xml version="1.0" encoding="UTF-8" standalone="no"?> <window xmlns="http://schemas.haulmont.com/cuba/window.xsd" xmlns:ext="http://schemas.haulmont.com/cuba/window-ext.xsd" extends="/com/haulmont/cuba/gui/app/security/user/edit/user-edit.xml"> <layout> <groupBox id="propertiesBox"> <grid id="propertiesGrid"> <rows> <row id="propertiesRow"> <fieldGroup id="fieldGroupLeft"> <column> <field ext:index="3" id="address" property="address"/> </column> </fieldGroup> </row> </rows> </grid> </groupBox> </layout> </window>
使用父界面的标识符在
screens.xml
中注册:<screen id="sec$User.edit" template="com/sample/sales/gui/extuser/extuser-edit.xml"/>
一旦上面提到的这些步骤都完成了,应用程序会使用
ExtUser
以及相应的界面替换平台中标准的User
实体和界面。界面控制器可以通过创建继承自界面控制器基类的新类进行扩展。类名需要在扩展的 XML 描述中通过根元素的
class
属性指定;上面提到的 XML 继承通用规则也会有效。 - 使用 CUBA Studio 扩展界面
-
这个例子中,我们将对应用程序组件示例中提到的客户管理组件的
Customer
实体界面进行扩展,为其客户浏览表格添加一个Excel
按钮,用来导出 Excel 表格。-
在 Studio 中创建一个新项目,并添加 Customer Manangemnt 组件。
-
在 CUBA 项目树中右键点击 Generic UI,右键菜单中选择 New > Screen。然后在 Screen Templates 标签页选择 Extend an existing screen。在 Extend Screen 列表中,选择
customer-browse.xml
。然后会在 web 模块创建新的ext-customer-browse.xml
和ExtCustomerBrowse.java
文件。 -
打开
ext-customer-browse.xml
切换到 Designer 标签页。父界面的组件会展示在设计器的工作区。 -
选择
customersTable
并添加一个新的excel
操作。 -
在
buttonsPanel
添加一个按钮,链接到customersTable.excel
操作。
最后,
ext-customer-browse.xml
的代码在 Text 标签页是如下:ext-customer-browse.xml<?xml version="1.0" encoding="UTF-8" standalone="no"?> <window xmlns="http://schemas.haulmont.com/cuba/screen/window.xsd" messagesPack="com.company.sales2.web" extends="com/company/customers/web/customer/customer-browse.xml"> <layout> <groupTable id="customersTable"> <actions> <action id="excel" type="excel"/> </actions> <buttonsPanel id="buttonsPanel"> <button id="excelButton" action="customersTable.excel"/> </buttonsPanel> </groupTable> </layout> </window>
再看看
ExtCustomerBrowse
界面控制器。ExtCustomerBrowse.java@UiController("customers_Customer.browse") @UiDescriptor("ext-customer-browse.xml") public class ExtCustomerBrowse extends CustomerBrowse { }
由于界面标识符
customers_Customer.browse
与父界面的标识符一样,因此,每次调用时会用新界面替换旧界面。 -
3.11.3. 扩展业务逻辑
平台业务逻辑的主要部分包含在 Spring bean 中。这样可以在应用程序中轻松地继承或重写它。
要替换 bean 实现,应该创建一个类来实现接口或继承平台基础类并在应用程序的spring.xml中注册它。不能将 @Component
注解用到于继承类;只能在 XML 配置中覆盖 bean。
下面是向PersistenceTools bean 添加方法的示例。
首先,创建一个带有必要方法的类:
public class ExtPersistenceTools extends PersistenceTools {
public Entity reloadInSeparateTransaction(final Entity entity, final String... viewNames) {
Entity result = persistence.createTransaction().execute(new Transaction.Callable<Entity>() {
@Override
public Entity call(EntityManager em) {
return em.reload(entity, viewNames);
}
});
return result;
}
}
在项目 core 模块的 spring.xml
中注册类,其标识符与平台 bean 相同:
<bean id="cuba_PersistenceTools" class="com.sample.sales.core.ExtPersistenceTools"/>
之后,Spring 上下文将始终返回 ExtPersistenceTools
实例而不是基类 PersistenceTools
的 实例。检查代码示例:
Persistence persistence;
PersistenceTools tools;
persistence = AppBeans.get(Persistence.class);
tools = persistence.getTools();
assertTrue(tools instanceof ExtPersistenceTools);
tools = AppBeans.get(PersistenceTools.class);
assertTrue(tools instanceof ExtPersistenceTools);
tools = AppBeans.get(PersistenceTools.NAME);
assertTrue(tools instanceof ExtPersistenceTools);
可以使用相同的逻辑来重写应用程序组件中的服务,例如:要替换 bean 实现,应该创建一个类,这个类扩展基础服务功能。在下面的示例中,创建了一个新类 NewOrderServiceBean
,并重写基类 OrderServiceBean
中的方法:
public class NewOrderServiceBean extends OrderServiceBean {
@Override
public BigDecimal calculateOrderAmount(Order order) {
BigDecimal total = super.calculateOrderAmount(order);
BigDecimal vatPercent = new BigDecimal(0.18);
return total.multiply(BigDecimal.ONE.add(vatPercent));
}
}
然后,如果在 spring.xml
中注册新创建的这个类,平台将会使用新的实现代替旧的实现 OrderServiceBean
。请注意,注册新的 bean 时,id 属性应该与应用程序组件中的基础服务的 id
一致,在 class
属性中指定新类的完全限定名:
<bean id="workshop_OrderService" class="com.company.retail.service.NewOrderServiceBean"/>
3.11.4. Servlet 和过滤器的注册
ServletRegistrationManager
bean 能够使用正确的类加载器动态注册 servlet 和过滤器,并允许使用类似于AppContext的静态类。它还保证对所有部署选项都能正确工作。
ServletRegistrationManager
有两个方法:
-
createServlet()
- 创建给定 servlet 类的 servlet。它使用从应用程序上下文对象获取的正确的ClassLoader
加载 servlet 类。这意味着新的 servlet 将能够使用平台的一些静态类,例如,AppContext
或Messages bean。 -
createFilter()
- 以相同的方式创建过滤器.
要使用这个 bean,建议在应用程序组件中创建一个托管的初始化 bean。这个 bean 应该使用 @Component
注解,并包含监听应用程序上下文初始化和销毁事件(ServletContextInitializedEvent 和 ServletContextDestroyedEvent
)的监听器。
初始化 bean 的一个示例:
@Component
public class WebInitializer {
@Inject
private ServletRegistrationManager servletRegistrationManager;
@EventListener
public void initializeHttpServlet(ServletContextInitializedEvent e) {
Servlet myServlet = servletRegistrationManager.createServlet(e.getApplicationContext(), "com.demo.comp.MyHttpServlet");
e.getSource().addServlet("my_servlet", myServlet)
.addMapping("/myservlet/");
}
}
这里的 WebInitializer
类只有一个事件监听器,用于从应用程序组件中给项目应用程序注册 HTTP servlet。
createServlet()
方法使用从 ServletContextInitializedEvent
获取的应用程序上下文和 HTTP servlet 的完全限定名。然后使用名称(my_servlet
)注册 servlet 并定义 HTTP-mapping(/myservlet/
)。现在,如果将此应用程序组件添加到应用程序中,将在初始化 servlet 和应用程序上下文后立即注册 MyHttpServlet
。
相关更复杂的示例,请参阅在应用程序组件中注册 DispatcherServlet部分。
- 单个 WAR 包部署模式下的 Servlet 注册
-
要在单 WAR 部署中正确加载自定义过滤器和 servlet,请按照以下步骤操作:
-
创建一个继承
javax.servlet.ServletContextListener
的类。这个类将负责 servlet 和过滤器的创建:public class CustomWebListener implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent servletContextEvent) { ServletContext servletContext = servletContextEvent.getServletContext(); registerServlet(servletContext); } @Override public void contextDestroyed(ServletContextEvent sce) { } protected void registerServlet(ServletContext servletContext) { Servlet testServlet = new TestServlet(); ServletRegistration.Dynamic servletReg = servletContext.addServlet("test_servlet", cubaServlet); servletReg.setLoadOnStartup(0); servletReg.setAsyncSupported(true); servletReg.addMapping("/testServlet"); } }
-
在
single-war-web.xml
文件中添加一个引用所创建类的新参数context-param
:<context-param> <param-name>webServletContextListener</param-name> <param-value>com.company.CustomWebListener</param-value> </context-param>
-
4. 应用程序开发
本章节从实践角度介绍了怎么开发基于 CUBA 框架的应用程序。
4.1. 推荐编码标准
代码格式
-
对于 Java 和 Groovey 代码,推荐按照 Java 代码规范 定义的编码标准。如果是在 IntelliJ IDEA 中编码,可以使用默认的编码格式,或者使用快捷键 Ctrl-Alt-L 进行代码格式化。
每行最大长度是 120 个字符,缩进 4 个字符,用空格替换 tab。
-
XML 代码: 缩进 4 个字符,用空格替换 tab。
命名规范
标识符 | 命名规则 | 示例 |
---|---|---|
Java 和 Groovy 类 |
||
界面控制器类 |
UpperCamelCase(首字母大写驼峰) 浏览界面控制器 编辑界面控制器 |
|
XML 界面描述文件 |
||
组件标识符,查询语句中参数名称 |
lowerCamelCase(首字母小写驼峰),只包含字母和数字 |
|
数据源标识符 |
lowerCamelCase(首字母小写驼峰),只包含字母和数字以 |
|
SQL 脚本 |
||
保留字 |
lowercase(小写) |
|
表名 |
UPPER_CASE(单词全大写下划线分隔)。名称需要以项目名称开头以区分命名空间。推荐表名使用单数形式,比如 ORDER,而不是 ORDERS |
|
列名 |
UPPER_CASE(单词全大写下划线分隔) |
|
外键列名 |
UPPER_CASE(单词全大写下划线分隔),包含此列指向的表名(去掉项目前缀)加上_ID 后缀。 |
|
索引名 |
UPPER_CASE(单词全大写下划线分隔),以 IDX_开头,包含带有项目名称的表名加上作为索引字段的名称。 |
|
4.2. 项目文件结构
以下是一个简单应用程序 Sales 的项目文件结构,由 Middleware 和 Web Client blocks 组成。
项目根目录包含构建脚本 build.gradle
和 settings.gradle
。
modules
目录包含项目的模块子目录 - global,core,web。
global 模块包含代码目录 src
,里面有三个配置文件 - metadata.xml,persistence.xml 和 views.xml。com.sample.sales.service
包里面有 Middleware 服务的接口代码;com.sample.sales.entity
包里面有实体类以及他们的本地消息文件。
core 模块包含以下目录:
-
db
– 包含数据库创建和更新脚本。 -
src
– 源代码目录;这个目录的根目录包含了 Middleware block 的应用程序属性文件和 spring.xml 配置文件。com.samples.sales.core
包包含了 Middleware 的类:services 的实现类,托管 beans 和 JMX beans。 -
test
- 中间件集成测试源码目录。 -
web
– 包含 Middleware block 构建的 web 应用程序的配置文件:context.xml 和 web.xml。
web 模块包含以下目录:
-
src
– 源代码目录,包含 Web Client block 的应用程序属性文件和其它配置文件 - web-menu.xml,web-permissions.xml,web-screens.xml 和 web-spring.xml。com.samples.sales.web
包内有 Web Client block(从DefaultApp
继承)的主要类定义和主要本地化语言包。 -
web
– 包含 Web Client block 构建的 web 应用程序的配置文件:context.xml 和 web.xml。
4.3. 构建脚本
基于 CUBA 框架的项目采用 Gradle 系统来构建。构建脚本是在项目根目录下的两个文件:
-
settings.gradle
– 定义项目名称和模块组。 -
build.gradle
– 构建配置文件。
本章节介绍构建脚本的结构以及 Gradle 任务参数说明。
4.3.1. build.gradle 的结构
本章节介绍 build.gradle
脚本的结构和主要元素。
- buildscript
- cuba
-
CUBA 特殊的构建逻辑封装在
cuba
Gradle 插件里。CUBA 插件在构建脚本的根节点引用,同时也需要在所有模块的configure
部分使用下面这个语句引用进来:apply(plugin: 'cuba')
cuba 插件的配置在
cuba
部分定义:cuba { artifact { group = 'com.company.sales' version = '0.1' isSnapshot = true } tomcat { dir = "$project.rootDir/build/tomcat" } ide { copyright = '...' classComment = '...' vcs = 'Git' } }
以下是一些可选的参数:
-
artifact
- 这里定义项目工件的分组和版本信息。工件的名称按照settings.gradle
里面设置的模块名称来设置。-
group
- 工件组 -
version
- 工件版本 -
isSnapshot
- 如果设置true
,工件名称会被添加SNAPSHOT
后缀。可以通过命令行参数来覆盖工件的版本,示例:
gradle assemble -Pcuba.artifact.version=1.1.1
-
-
tomcat
- 这部分定义了用来 快速部署的 Tomcat 服务的设置。-
dir
- Tomcat 的 安装目录。 -
port
- Tomcat 监听端口,默认 8080。 -
debugPort
- Java 调试监听端口,默认 8787。 -
shutdownPort
- 监听SHUTDOWN
命令的端口,默认 8005。 -
ajpPort
- AJP 连接器端口,默认 8009。
-
-
ide
- 这部分包含 Studio 和 IDE 的内容-
vcs
- 项目的版本控制系统配置,目前只支持Git
或者svn
。 -
copyright
- 插入到每个源文件开头的版权信息。 -
classComment
- Java 源文件类声明开头插入的注释信息。
-
-
uploadRepository
- 这部分定义了使用uploadArchives
任务上传打包的项目工件目标 仓库的设置。-
url
- 仓库的地址 URL。如果不设置,默认会使用 Haulmont 的仓库地址。 -
user
- 访问仓库的用户名。 -
password
- 访问仓库的密码。也可以通过命令行参数的方式给这个上传仓库的任务提供参数:
gradlew uploadArchives -PuploadUrl=http://myrepo.com/content/repositories/snapshots -PuploadUser=me -PuploadPassword=mypassword
-
-
- dependencies
-
这部分包含了项目中使用的一组应用程序组件。组件通过他们各自的 global 模块来指定。在下面的例子中,使用了三个组件:
com.haulmont.cuba
(cuba 平台组件),com.haulmont.reports
(reports premium 组件) 和com.company.base
(自定义组件):dependencies { appComponent("com.haulmont.cuba:cuba-global:$cubaVersion") appComponent("com.haulmont.reports:reports-global:$cubaVersion") appComponent("com.company.base:base-global:0.1-SNAPSHOT") }
- configure
-
configure
部分包含模块的配置。其中最主要的部分就是声明依赖,示例:configure(coreModule) { dependencies { // standard dependencies using variables defined in the script above compile(globalModule) provided(servletApi) jdbc(hsql) testRuntime(hsql) // add a custom repository-based dependency compile('com.company.foo:foo:1.0.0') // add a custom file-based dependency compile(files("${rootProject.projectDir}/lib/my-library-0.1.jar")) // add all JAR files in the directory to dependencies compile(fileTree(dir: 'libs', include: ['*.jar'])) }
entitiesEnhancing
配置模块用来对实体类进行字节码增强(weaving - 也叫织入),至少需要在 global 模块声明这个任务,也可以在其它模块分别声明。这里的
main
和test
分别是项目和测试的代码目录,可选的persistenceConfig
参数用来指定各自的 persistence.xml 文件。如果这个可选参数没设置,这个任务会对 CLASSPATH 里能找到的*persistence.xml
文件中的所有实体做增强。configure(coreModule) { ... entitiesEnhancing { main { enabled = true persistenceConfig = 'custom-persistence.xml' } test { enabled = true persistenceConfig = 'test-persistence.xml' } } }
非标准的模块依赖可以在 Studio 中通过 CUBA 项目视图的 Project properties 部分来设置。
对于动态版本依赖和版本冲突的问题,可以用 Maven 对此场景的解决方法。按照这个方法,正式版本的优先级会高于快照版本,而且越新的版本有越精确的版本编号。所有条件都一样的情况下,版本编号按照字母表的顺序定优先级,示例:
|
4.3.2. 配置仓库连接
- 主仓库
-
当创建项目的时候,需要选择包含 CUBA 工件的主仓库。默认情况下有两种选择(如果配置了私仓的话就有更多选择):
-
https://repo.cuba-platform.com/content/groups/work
- Haulmont 服务器提供的仓库。需要在构建脚本中指定通用的密钥:(cuba
/cuba123
)。 -
https://dl.bintray.com/cuba-platform/main
- JFrog Bintray提供的仓库,支持匿名访问。
这两个仓库有相同的最新平台版本的工件内容,但是 Bintray 不包含版本快照(snapshots)。对于全球访问来说,Bintray 应当更加可靠。
使用 Bintray 的情况下,新项目的构建脚本会配置成分别使用 Maven Central,JCenter 和 Vaadin 插件仓库。
-
- 自定义仓库
-
项目可以包含任意数量的自定义仓库,这些仓库可以包含应用程序组件。需要在
build.gradle
里手动将这些仓库添加到主仓库 之后 的位置,示例:repositories { // main repository containing CUBA artifacts maven { url 'https://repo.cuba-platform.com/content/groups/work' credentials { // ... } } // custom repository maven { url 'http://localhost:8081/repository/maven-snapshots' } }
4.3.3. 构建任务
Tasks - 任务 是 Gradle 的可执行单元。任务可以在插件内定义或者在构建脚本中定义。以下是一些 CUBA 特定的任务,任务的参数可以通过 build.gradle
来配置。
4.3.3.1. 构建信息
CUBA gradle 插件自动在 global
模块的配置里添加 buildInfo
任务。这个任务会在 build-info.properties
文件里写入应用程序的信息,然后这个文件会被打包到 global
工件(app-global-1.0.0.jar
)里。BuildInfo
bean 在运行时会读取这些信息,然后显示在 Help > About 窗口。其它的业务机制也可以调用这个 bean,以便于获取应用程序名称,版本之类的信息。
下面这些任务参数可以根据需要指定:
-
appName
- 应用程序名称。默认值会从settings.gradle
中的项目名称(project name)读取。 -
artifactGroup
- 工件组,一般习惯上会使用项目的包名根目录。 -
version
- 应用程序版本。默认值是通过cuba.artifact.version
属性设定的。 -
properties
- 一组任意的属性键值对,默认是空。
使用自定义的 buildInfo
任务示例:
configure(globalModule) {
buildInfo {
appName = 'MyApp'
properties = ['prop1': 'val1', 'prop2': 'val2']
}
// ...
4.3.3.2. 构建 UberJar
buildUberJar
– CubaUberJarBuilding
类型的任务,会创建一个包含应用程序代码和所有依赖包在一起并且还有嵌入的 Jetty Http 服务的 JAR 包。可以选择创建一个大而全的包含所有的 JAR 包,或者选择给每一个应用程序 block 创建单独的 JAR 包,例如可以给中间件(middleware)创建 app-core.jar
,给 web 客户端创建 app.jar
。
这个任务需要在 build.gradle
的根节点声明。生成的 JAR 包会放在项目的 build/distributions
目录。参考 UberJAR 部署 章节了解怎么运行生成的 JAR 包。
可以通过 Studio 里的 Deployment > UberJAR settings 界面配置这个任务。 |
任务参数:
-
coreJettyEnvPath
- 必要参数,设置一个项目根目录的相对路径,这个路径需要指向一个包含 JNDI resource 的配置文件,这些配置是给 Jetty Http Server 用的。配置文件里至少需要包含一份关于主数据库的 JDBC 数据源定义。Studio 可以根据配置的数据库连接参数生成这个文件。task buildUberJar(type: CubaUberJarBuilding) { coreJettyEnvPath = 'modules/core/web/META-INF/jetty-env.xml' // ... }
还可以使用不同的 jetty-env.xml 文件和
-jettyEnvPath
命令行参数在运行时为同一个 UberJar 提供不同的数据库设置。 -
appProperties
- 应用程序属性的键值对。这里面提供的属性键值对会以生成的 JAR 包里的WEB-INF/local.app.properties
文件定义的属性为基础添加。task buildUberJar(type: CubaUberJarBuilding) { appProperties = ['cuba.automaticDatabaseUpdate' : true] // ... }
-
singleJar
- 如果设置成true
,会创建一个包含所有模块(core,web,portal)的 JAR 包。默认是false
。task buildUberJar(type: CubaUberJarBuilding) { singleJar = true // ... }
-
webPort
- 单一 JAR 包(singleJar=true
)或者 Web 模块的 JAR 包的 Http 服务端口,默认是8080
。也可以在运行时通过-port
命令行参数动态指定。 -
corePort
- core 模块 JAR 包的 Http 服务端口,默认是8079
。也可以在运行时启动相应的 JAR 包的时候用命令行参数-port
来指定。 -
portalPort
- partal 模块 JAR 包的 Http 服务端口,默认是8081
。也可以在运行时启动相应的 JAR 包的时候用命令行参数-port
来指定。 -
appName
- 应用程序名称,默认是app
。可以在 Studio 中通过 Project Properties 窗口里的 Module prefix 来给整个项目设置名称,或者也可以通过这个参数只给buildUberJar
任务设置。例如:task buildUberJar(type: CubaUberJarBuilding) { appName = 'sales' // ... }
当把应用程序名称改成
sales
之后,这个任务会生成sales-core.jar
和sales.jar
文件,web 客户端可以通过http://localhost:8080/sales
访问。还能通过运行时的-contextName
命令行参数改变 web 上下文,而不需要修改应用程序名称,甚至直接修改 JAR 包的名字也行。 -
logbackConfigurationFile
- 日志配置文件的相对目录。比如:
logbackConfigurationFile = "/modules/global/src/logback.xml"
-
useDefaultLogbackConfiguration
- 当设置成true
(也是默认值)的时候,这个任务会拷贝标准的logback.xml
配置文件。 -
webJettyConfPath
- Jetty Server 配置文件的相对路径,可以给 UberJar(singleJar=true
)或者 web JAR(singleJar=false
)配置。参考: https://www.eclipse.org/jetty/documentation/9.4.x/jetty-xml-config.html -
coreJettyConfPath
- core JAR(singleJar=false
)的 Jetty Server 配置文件相对目录,要注意跟上面描述的coreJettyEnvPath
相区别。 -
portalJettyConfPath
- portal JARsingleJar=false
)的 Jetty Server 配置文件相对目录。 -
coreWebXmlPath
- 用来作为 core 模块web.xml
的文件的相对目录。 -
webWebXmlPath
- 用来作为 web 模块web.xml
的文件的相对目录。 -
portalWebXmlPath
- 用来作为 portal 模块web.xml
的文件的相对目录。 -
excludeResources
- 正则表达式,表示不需要包含在 JAR 包里面的那些 resource 文件。 -
mergeResources
- 正则表达式,表示需要整合在 JAR 包里面的那些 resource 文件。 -
webContentExclude
- 正则表达式,表示不需要包含在 web content 里面的那些文件。 -
coreProject
- 用来作为 core 模块(Middleware)的 Gradle 项目。如果没定义,则会使用标准的 core 模块。 -
webProject
- 用来作为 web 模块(Web Client)的 Gradle 项目。如果没定义,则会使用标准的 web 模块。 -
portalProject
- 用来作为 portal 模块(Web Portal)的 Gradle 项目。如果没定义,则会使用标准的 portal 模块。 -
frontProject
- 用来作为 Polymer UI 模块的 Gradle 项目。如果没定义,则会使用标准的 polymer-client 模块。 -
polymerBuildDir
- Polymer UI 构建生成的目录名称。默认是es6-unbundled
。如果在polymer.json
里面修改了预设的编译参数的话,需要提供这个参数。
4.3.3.3. 构建 War
buildWar
– CubaWarBuilding
类型的任务,会构建一个 WAR 包,包含所有的应用程序代码和所有的依赖包。需要在 build.gradle
文件的根目录声明。生成的 WAR 包会在项目的 build/distributions
目录。
可以通过 Studio 里的 Deployment > WAR Settings 界面配置这个任务。 |
任何 CUBA 应用程序包含至少两个 block:中间件(Middleware)和 web 客户端(Web Client)。所以部署程序最自然的方法就是创建两个单独的 WAR 包:中间件一个,web 客户端一个。这样做的话也可以在用户数量增长的时候方便进行扩展。但是单独的包会重复包含一些两边都依赖的包,因此会增加整个程序的大小。还有就是扩展的部署选择也不常用,反而会使得部署流程更加复杂。CubaWarBuilding
任务可以创建这两种类型的 WAR 包:每个 block 一个包,或者包含两个 block 的包,后面这种情况下,应用程序的那些 block 会在一个 web 应用程序里面被不同的类加载器加载。
- 给中间件和 web 客户端创建不同的 WAR 包
-
可以采用这样的 task 配置:
task buildWar(type: CubaWarBuilding) { appHome = '${app.home}' appProperties = ['cuba.automaticDatabaseUpdate': 'true'] singleWar = false }
任务参数:
-
appName
- web 应用程序的名称,默认值是读取 Modules prefix,比如app
。 -
appHome
– 指向应用程序主目录。可以使用绝对目录,或者相对于 home 目录的相对目录,亦或是 Java 系统变量的占位符,系统变量需要在服务启动时设置。 -
appProperties
- 定义应用程序属性键值对。这些属性会以 WAR 包里面的/WEB-INF/local.app.properties
文件为基础增加。appProperties = ['cuba.automaticDatabaseUpdate': 'true']
会在应用程序第一次启动的时候创建数据库,如果没有现成的数据库,需要设置这个参数。 -
singleWar
- 如果要为 blocks 构建单独的 WAR,需要设置这个为false
。 -
includeJdbcDriver
- 是否包含项目里当前使用的 JDBC 驱动,默认是false
。 -
includeContextXml
- 是否包含项目里当前使用的 Tomcat 的context.xml
文件,默认值是false
。 -
coreContextXmlPath
- 如果includeContextXml
设置为true
,使用这个参数设置一个配置文件的相对目录用来替换项目context.xml
文件。 -
hsqlInProcess
- 如果设置成true
,context.xml
里面的数据库 URL 会按照 HSQL in-process 模式修改。 -
coreProject
- 用来作为 core 模块(Middleware)的 Gradle 项目。如果没定义,则会使用标准的 core 模块。 -
webProject
- 用来作为 web 模块(Web Client)的 Gradle 项目。如果没定义,则会使用标准的 web 模块。 -
portalProject
- 用来作为 portal 模块(Web Portal)的 Gradle 项目。如果项目包含了 portal 模块,需要设置这个参数,比如,portalProject = project(':app-portal')
。 -
coreWebXmlPath
,webWebXmlPath
,portalWebXmlPath
- 用来作为相应模块web.xml
的文件的相对目录。使用自定义
web.xml
文件的例子:task buildWar(type: CubaWarBuilding) { singleWar = false // ... coreWebXmlPath = 'modules/core/web/WEB-INF/production-web.xml' webWebXmlPath = 'modules/web/web/WEB-INF/production-web.xml' }
-
logbackConfigurationFile
- 日志配置文件的相对目录。示例:
logbackConfigurationFile = "/modules/global/src/logback.xml"
-
useDefaultLogbackConfiguration
- 当设置成true
(也是默认值)的时候,这个任务会拷贝标准的logback.xml
配置文件。 -
polymerBuildDir
- Polymer UI 构建生成的目录名称。默认是es6-unbundled
。如果在polymer.json
里面修改了预设的编译参数的话,需要提供这个参数。
-
- 创建单一的 WAR 包
-
需要创建包含中间件和 web 客户端 block 的单一 WAR 包,可以用下面这个配置:
task buildWar(type: CubaWarBuilding) { appHome = '${app.home}' webXmlPath = 'modules/web/web/WEB-INF/single-war-web.xml' }
除了上面这些参数之外,以下这些参数也可以使用:
-
singleWar
- 不设置或者设置成true
。 -
webXmlPath
- 用来作为单一 WAR 包web.xml
的文件相对目录。这个文件定义了两个 servlet 上下文的监听器,用来加载程序 block:SingleAppCoreServletListener
和SingleAppWebServletListener
。通过上下文参数给这两个监听器传递所需要的所有参数。single-war-web.xml
示例:<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0"> <!--Application components--> <context-param> <param-name>appComponents</param-name> <param-value>com.haulmont.cuba</param-value> </context-param> <!-- Web Client parameters --> <context-param> <description>List of app properties files for Web Client</description> <param-name>appPropertiesConfigWeb</param-name> <param-value> classpath:com/company/sample/web-app.properties /WEB-INF/local.app.properties </param-value> </context-param> <context-param> <description>Web resources version for correct caching in browser</description> <param-name>webResourcesTs</param-name> <param-value>${webResourcesTs}</param-value> </context-param> <!-- Middleware parameters --> <context-param> <description>List of app properties files for Middleware</description> <param-name>appPropertiesConfigCore</param-name> <param-value> classpath:com/company/sample/app.properties /WEB-INF/local.app.properties </param-value> </context-param> <!-- Servlet context listeners that load the application blocks --> <listener> <listener-class> com.vaadin.server.communication.JSR356WebsocketInitializer </listener-class> </listener> <listener> <listener-class> com.haulmont.cuba.core.sys.singleapp.SingleAppCoreServletListener </listener-class> </listener> <listener> <listener-class> com.haulmont.cuba.web.sys.singleapp.SingleAppWebServletListener </listener-class> </listener> </web-app>
单个 WAR 包部署时所有的 sevlets 和 filters 需要通过编程的方式注册,参考单一 WAR 包部署注册 Servlets。
单一 WAR 包只包含 core 和 web 客户端 模块。如果需要部署 portal 模块,需要用另外的 WAR 包。
-
参考 部署 WAR 至 Jetty 章节详细介绍 WAR 部署的各种情况和步骤。
4.3.3.4. 构建 WidgetSet
buildWidgetSet
- CubaWidgetSetBuilding
类型的任务,如果项目中有 web-toolkit
模块的话,可以用这个任务构建一个自定义的 GWT WidgetSet。这个模块可以用来开发自定义可视化组件。
可用参数:
-
style
- 脚本的输出样式:OBF
,PRETTY
或者DETAILED
。默认是OBF
。 -
logLevel
- 日志级别:ERROR
,WARN
,INFO
,TRACE
,DEBUG
,SPAM
, 或者ALL
。默认是INFO
。 -
draft
- 使用最小优化进行快速编译。默认值false
。
使用示例:
task buildWidgetSet(type: CubaWidgetSetBuilding) {
widgetSetClass = 'com.company.sample.web.toolkit.ui.AppWidgetSet'
style = 'PRETTY'
}
4.3.3.5. 创建数据库
createDb
– CubaDbCreation
类型的任务,通过执行相应的数据库脚本来创建应用程序数据库。在 build.gradle
里的 core 模块里面声明这个任务。支持参数:
-
dbms
– 数据库类型,hsql
,postgres
,mssql
或者oracle
。 -
dbName
– 数据库名称 -
dbUser
– 数据库用户名 -
dbPassword
– 用户的密码 -
host
– 数据库服务的地址和端口(可选),使用host[:port]
格式。如果没设置,则会使用localhost
。 -
connectionParams
- 可选的连接参数,添加到连接 URL 最后面。 -
masterUrl
– 数据库连接串 URL。如果没设置,默认会根据dbms
和host
生成。 -
dropDbSql
– 删除数据库的 SQL 命令。如果没设置,默认会根据dbms
生成。 -
createDbSql
– 创建数据库的 SQL 命令。如果没设置,默认会根据dbms
生成。 -
driverClasspath
– 包含 JDBC 驱动的 JAR 包文件列表。在 Linux 系统使用 ":" 分隔列表里的文件,在 Windows 系统使用 ";" 分隔。如果没设置,系统会用当前模块的jdbc
配置所需要的依赖。使用 Oracle 的时候需要显式的定义driverClasspath
,因为 Oracle 的 JDBC 驱动没有能自动下载的可用依赖,需要手动配置。 -
oracleSystemPassword
– Oracle 的 SYSTEM 用户密码。
PostgreSQL 示例:
task createDb(dependsOn: assemble, description: 'Creates local database', type: CubaDbCreation) {
dbms = 'postgres'
dbName = 'sales'
dbUser = 'cuba'
dbPassword = 'cuba'
}
微软 SQL Server 示例:
task createDb(dependsOn: assemble, description: 'Creates local database', type: CubaDbCreation) {
dbms = 'mssql'
dbName = 'sales'
dbUser = 'sa'
dbPassword = 'saPass1'
connectionParams = ';instance=myinstance'
}
Oracle 示例:
task createDb(dependsOn: assemble, description: 'Creates database', type: CubaDbCreation) {
dbms = 'oracle'
host = '192.168.1.10'
dbName = 'orcl'
dbUser = 'sales'
dbPassword = 'sales'
oracleSystemPassword = 'manager'
driverClasspath = "$tomcatDir/lib/ojdbc6.jar"
}
4.3.3.6. 调试 WidgetSet
debugWidgetSet
- CubaWidgetSetDebug
类型的任务,启动 GWT 代码服务器(Code Server)用来在浏览器里面调试 widgets。
使用示例:
task debugWidgetSet(type: CubaWidgetSetDebug) {
widgetSetClass = 'com.company.sample.web.toolkit.ui.AppWidgetSet'
}
需要确保 web-toolkit
模块在 runtime
配置里有一个 Servlet API 库的依赖:
configure(webToolkitModule) {
dependencies {
runtime(servletApi)
}
...
参考 调试 web Widgets 查询怎么在浏览器里面调试代码。
4.3.3.7. 部署
deploy
– CubaDeployment
类型的任务,快速部署一个模块到 Tomcat。在 build.gradle 里的 core,web 和 portal 模块中声明。可用参数:
-
appName
– 从模块创建的 web 应用程序名称。实际上是tomcat/webapps
一个子目录的名称。 -
jarNames
– 构建一个模块时生成的 JAR 文件(不带版本号)列表。这些 JAR 文件需要放在 web 程序的WEB-INF/lib
目录里。所有其它的模块工件(artifacts)和依赖包将会放在tomcat/shared/lib
。
示例:
task deploy(dependsOn: assemble, type: CubaDeployment) {
appName = 'app-core'
jarNames = ['cuba-global', 'cuba-core', 'app-global', 'app-core']
}
4.3.3.8. 部署样式主题
deployThemes
- CubaDeployThemeTask
类型的任务,构建和部署项目内定义的主题至目前已经 部署且在运行的 web 应用程序中。不需要重启服务的情况下,主题的改动就能生效。
示例:
task deployThemes(type: CubaDeployThemeTask, dependsOn: buildScssThemes) {
}
4.3.3.9. 部署 War
deployWar
- CubaJelasticDeploy
类型的任务,部署 WAR 包到 Jelastic 服务器。
示例:
task deployWar(type: CubaJelasticDeploy, dependsOn: buildWar) {
email = '<your@email.address>'
password = '<your password>'
context = '<app contex>'
environment = '<environment name or ID>'
hostUrl = '<Host API url>'
}
任务参数:
-
appName
- web 应用程序的名称。默认是使用模块前缀,比如app
。 -
email
- Jelastic 服务的登录名。 -
password
- Jelastic 账号的密码 -
context
- 应用程序上下文。默认值ROOT
。 -
environment
- 部署 WAR 的环境(environment)。可以设置成环境的名称或者 ID。 -
hostUrl
- API 服务的地址,典型值app.jelastic.<host name>
。 -
srcDir
- WAR 包放置的目录。默认是"${project.buildDir}/distributions/war"
。
4.3.3.10. 重启服务
restart
– 此任务会关停本地 Tomcat 服务,然后运行快速部署,再启动本地 Tomcat 服务。
4.3.3.11. 配置 Tomcat
setupTomcat
– CubaSetupTomcat
类型的任务,为应用程序的快速部署方式安装并且初始化本地 Tomcat。当使用 cuba Gradle 插件的时候,此任务会自动创建并添加到项目中,所以不需要在 build.gradle
中声明这个任务。Tomcat 的安装目录通过 cuba
任务的 tomcat.dir
来指定。默认值是项目的 build/tomcat
子目录。
4.3.3.12. 启动 Tomcat 服务
start
– CubaStartTomcat
类型的任务,用来启动在 setupTomcat 任务中安装的本地 Tomcat 服务。当使用 cuba Gradle 插件的时候,此任务会自动创建并添加到项目中,所以不需要在 build.gradle
中声明这个任务。
4.3.3.13. 启动本地 HSQL 数据库
startDb
– CubaHsqlStart
类型的任务,用来启动本地 HSQLDB 服务。 任务参数:
-
dbName
– 数据库名称,默认cubadb
。 -
dbDataDir
– 数据库目录,默认是项目的deploy/hsqldb
子目录。 -
dbPort
– 数据库服务端口号,默认是9001
。
示例:
task startDb(type: CubaHsqlStart) {
dbName = 'sales'
}
4.3.3.14. 停止 Tomcat 服务
stop
– CubaStopTomcat
类型的任务,用来停止在 setupTomcat 任务中安装的本地 Tomcat 服务。当使用 cuba Gradle 插件的时候,此任务会自动创建并添加到项目中,所以不需要在 build.gradle
中声明这个任务。
4.3.3.16. 打开 Tomcat 窗口
tomcat
– Exec
类型的任务,在打开的终端窗口内运行本地 Tomcat 服务,即便启动失败,终端窗口也会打开。这个任务在调试 Tomcat 问题的时候会很有用,比如在服务启动时由于 Java 版本不匹配引起的启动失败。
4.3.3.17. 更新数据库
updateDb
– CubaDbUpdate
类型的任务,通过执行相应的数据库脚本文件来更新数据库。跟 createDb
任务类似,只是没有 dropDbSql
和 createDbSql
这两个参数。
4.3.4. 启动构建任务
构建脚本中定义的 Gradle 任务可以通过如下方式启动:
-
如果是在 CUBA Studio 中使用项目,很多从 CUBA 主菜单执行的命令都实际上代理给 Gradle 任务:Build Tasks 菜单下的所有命令,以及 Start/Stop/Restart Application Server 和 Create/Update Database 命令。
-
或者,也可以通过执行项目中的
gradlew
脚本(Gradle Wrapper)来执行任务。 -
还有一个方式就是使用手动安装的 Gradle 版本 4.10.3。这种情况可以直接执行 Gradle 安装目录下
bin
子目录里的gradle
可执行文件。
比如,通过执行以下命令可以编译 Java 文件,并且构建项目的 JAR 包:
gradlew assemble
./gradlew assemble
如果项目用了 Premium 插件,并且不是用 Studio 启动项目构建,那么需要给 Gradle 传递 Premium 插件的密钥,可以参考 这里有更多细节。 |
按照通常使用的顺序,以下是一些典型需要使用的构建任务。
-
assemble
– 编译 Java 文件并且构建项目工件的 JAR 包,存在各个模块的build
子目录。 -
clean
– 删除项目各个模块下的build
子目录。 -
setupTomcat – 安装并配置 Tomcat 到
build.gradle
脚本中cuba.tomcat.dir
属性定义的路径。 -
deploy – 部署应用程序到 Tomcat 服务。Tomcat 服务是之前通过
setupTomcat
任务预先安装的。 -
start – 启动 Tomcat 服务。
-
stop – 停止已经启动的 Tomcat 服务。
-
restart – 顺序执行
stop
,deploy
,start
这三个任务。
4.3.5. 安装配置私仓
这节介绍了怎样安装一个 Maven 私仓,使用这个私仓替换 CUBA 公共仓库,并且用来存储平台工件和其它的依赖。以下这些情况推荐使用私仓:
-
在不稳定或者很慢的互联网环境。尽管 Gradle 会在开发者的机器上缓存下载下来的工件,但是时不时的还是需要连接到工件仓库,比如当第一次运行构建或者在一个新版本的平台上开始构建。
-
由于组织级别的安全措施,不能直接访问互联网。
-
不打算为 CUBA Premium 插件续费,但是将来还需要使用已经下载过的付费插件构建项目。
以下是安装和配置私仓的步骤:
-
在有互联网的网络安装仓库管理软件。
-
配置私仓作为 CUBA 公共仓库的代理。
-
修改项目构建脚本使用私仓。可以通过 Studio 或者直接修改
build.gradle
。 -
启动完整项目构建流程,以此来在私仓中缓存所有必须的工件。
4.3.5.1. 安装仓库管理软件
这里使用 Sonatype Nexus OSS 仓库管理软件作为示例。
- 在微软 Windows 操作系统
-
-
下载 Sonatype Nexus OSS 版本 2.x (2.14.3 测试通过)
-
解压压缩包至
c:\nexus-2.14.3-02
-
修改配置文件内容
c:\nexus-2.14.3-02\conf\nexus.properties
:-
可以配置服务器端口,默认是 8081
-
配置仓库数据目录:
替换掉
nexus-work=${bundleBasedir}/../sonatype-work/nexus
使用方便缓存数据的目录,比如
nexus-work=${bundleBasedir}/nexus/sonatype-work/content
-
-
切换到目录
c:\nexus-2.14.3-02\bin
-
安装 wrapper(以管理员身份运行),用来以服务的方式启动和停止 Nexus:
nexus.bat install
-
启动 nexus 服务
-
在浏览器打开
http://localhost:8081/nexus
,用默认的管理员账号登录:用户名admin
,密码admin123
。
-
- 使用 Docker
-
另外,我们也可以用 Docker 来简化本地使用的安装步骤。也可以在 Docker Hub 找到介绍。
-
运行
docker pull sonatype/nexus:oss
下载最新稳定的 OSS 镜像 -
使用
docker run -d -p 8081:8081 --name nexus sonatype/nexus:oss
命令构建容器 -
Docker 容器会在几分钟内运行 nexus。用以下任意方法测试:
-
curl http://localhost:8081/nexus/service/local/status
-
浏览器访问
http://localhost:8081/nexus
。
-
-
密钥是一样的:用户名:
admin
,密码:admin123
。
-
4.3.5.2. 配置代理仓库
点击左边面板的 Repositories 连接。
在打开的 Repositories 页,点击 Add 按钮,然后选择 Proxy Repository。此处会添加一个新的仓库,在 Configuration 标签页填写所需的信息:
-
Repository ID:
cuba-work
-
Repository Name:
cuba-work
-
Provider:
Maven2
-
Remote Storage Location:
https://repo.cuba-platform.com/content/groups/work
-
Auto Blocking Enabled:
false
-
启用 Authentication, 设置 Username:
cuba
, Password:cuba123
-
点击 Save 按钮.
创建一个仓库组(Repository Group):在 Nexus 点击 Add 按钮,然后选择 Repository Group 然后在 Configuration 标签页填写:
-
Group ID:
cuba-group
-
Group Name:
cuba-group
-
Provider:
Maven2
-
从 Available Repositories 添加仓库 cuba-work 到 Ordered Group Repositories
-
点击 Save 按钮
如果订购了 Premium 插件服务,可以添加一个 premium 的仓库:
-
Repository ID:
cuba-premium
-
Repository Name:
cuba-premium
-
Provider:
Maven2
-
Remote Storage Location:
https://repo.cuba-platform.com/content/groups/premium
-
Auto Blocking Enabled:
false
-
启用 Authentication,使用授权码的前半部分(短横之前的部分)作为 Username,后半部分(短横之后的部分)作为 Password。
-
点击 Save 按钮。
-
点击 Refresh 按钮。
-
选择 cuba-group 组。
-
在 Configuration 标签页,添加 cuba-premium 仓库到组里,放到 cuba-work 之后。
-
点击 Save 按钮。
4.3.5.3. 使用私仓
至此私仓已经可以用了。在界面上方显示 cuba-group 的 URL,比如:
http://localhost:8081/nexus/content/groups/cuba-group
-
如果创建新项目,点击 New project 窗口中 Repository 旁边的按钮。
-
对于已有的项目,编辑 Project properties 然后点击 Repository 旁边的按钮。
-
在弹出框中,点击 Add,输入私仓地址 URL 和用户名密码:
admin / admin123
。 -
选择新仓库,点击 OK 按钮后就可以项目中使用这个仓库。
-
构建(build)项目。
在初次构建的过程中,新仓库会下载必要的工件(artifacts)并且存在缓存以供下次使用。可以从 c:\nexus-2.14.3-02\sonatype-work
找到这些文件。
4.3.5.4. 孤立网络中的私仓
如果需要在没有互联网连接的环境开发 CUBA 应用,可以这样:
-
在网络中安装私仓管理工具。
-
从有网的环境拷贝私仓缓存的内容到孤立网络。如果是按照之前的步骤安装的软件,拷贝的内容保存在:
c:\nexus-2.14.3-02\sonatype-work
-
重启 nexus 服务。
如果需要在孤立网络中添加新平台版本的工件,到有网的环境,通过有网的仓库进行一次基于新版本的构建,然后再拷贝这些下载下来的新版本工件到孤立网络的私仓。
4.4. 创建项目
推荐使用 CUBA Studio 创建新项目。可以参考本手册的快速开始章节。
另一个方式是通过 CUBA CLI 创建:
-
打开终端并且启动 CUBA CLI。
-
输入命令
create-app
。可以用 tab 键自动补全命令。 -
CLI 会询问项目配置。按回车键使用默认值,或者也可以自定义配置:
-
Project name – 项目名称。对于示例项目,CLI 会生成随机名称可以用来做默认选项。
-
Project namespace – 命名空间,用来做实体名称和数据库表名称的前缀。命名空间只能由拉丁字母组成,越短越好。
-
Platform version – 项目中使用的平台版本。平台工件会在项目构建的过程中从仓库自动下载。
-
Root package – Java 类的包名根目录。
-
Database – 使用的 SQL 数据库。
-
完成之后,在当前目录的一个新目录会创建这个空项目。可以使用 Studio、CLI 或者任意其它 IDE 继续开发。
4.5. 使用应用程序组件
在 CUBA 市场 上发布的应用程序组件被称为扩展组件(add-on),因为它们扩展了框架的功能,并且可以在任何基于 CUBA 的应用程序中使用。
4.5.1. 使用公共扩展组件
可以通过以下方式将发布在市场上的扩展组件添加到项目中。第一种和第二种方法假设你使用的是一个标准 CUBA 仓库。最后一种方法适用于开源扩展组件,不涉及任何远程仓库。
- 通过 Studio
-
-
编辑 Project properties 并在 App components 面板上单击 Custom components 旁边的加号按钮。
-
从 Marketplace 页面或扩展组件的文档中复制扩展组件的 Maven 坐标,并将其粘贴到坐标输入框中,例如:
com.haulmont.addon.cubajm:cuba-jm-global:0.3.1
-
单击对话框中的 OK。Studio 将尝试在当前项目选择的仓库中查找扩展组件的二进制文件。如果找到,对话框将关闭,扩展组件将显示在自定义组件列表中。
-
单击 OK 保存项目属性。
-
- 通过手动编辑
-
-
编辑
build.gradle
并在根dependencies
元素中指定扩展组件的坐标:dependencies { appComponent("com.haulmont.cuba:cuba-global:$cubaVersion") // your add-ons go here appComponent("com.haulmont.addon.cubajm:cuba-jm-global:0.3.1") }
-
在命令行中执行
gradlew idea
以在项目的开发环境中包含扩展组件。 -
编辑
core
和web
模块的web.xml
文件,并将扩展组件的标识符(等同于 Maven groupId)添加到appComponents
上下文参数中以空格分隔的应用程序组件列表:<context-param> <param-name>appComponents</param-name> <param-value>com.haulmont.cuba com.haulmont.addon.cubajm</param-value> </context-param>
-
- 通过源码构建
-
-
将扩展组件的仓库克隆到本地目录,然后将项目导入 Studio。
-
执行 CUBA > Advanced > Install app component 主菜单命令将扩展组件安装到本地 Maven 仓库(默认为
~/.m2
目录)。 -
在 Studio 中打开项目,然后在 Project > Properties 中选上 Use local Maven repository。
-
在 App components 面板上,单击 Custom components 旁边的加号按钮,然后在对话框底部的下拉列表中选择扩展组件。扩展组件的坐标将显示在顶部的字段中。
-
单击对话框中的 OK 并保存项目属性。
-
4.5.2. 创建应用程序组件
如果正在开发可复用的应用程序组件,本节包含一些有用的建议。
- 命名规则
-
-
使用标准的反转域名表示法选择根 java 包,例如
com.jupiter.amazingsearch
。根包不应该以任何其它组件或应用程序的根包开头。例如,如果有一个带有
com.jupiter.tickets
根包的应用程序,则不能将com.jupiter.tickets.amazingsearch
包用于组件。原因是 Spring 从指定的根包开始扫描 bean 的类路径,并且每个组件的扫描空间必须是唯一的。 -
命名空间用作数据库表的前缀,因此对于公共组件,应该是一个表示综合含义的单词(或缩写),如
jptams
,而不仅仅是search
。这样可以将目标应用程序中命名冲突的风险降到最低。不能在命名空间中使用下划线和短横,只能使用字母和数字。 -
模块前缀应该与命名空间名称一致,但可以包含短横,如
jpt-amsearch
。 -
使用命名空间作为 bean 名称和应用程序属性的前缀,例如:
@Component("jptams_Finder") @Property("jptams.ignoreCase")
-
- 安装到本地 Maven 仓库
-
要使组件可用于本地计算机上的项目,请通过执行 CUBA > Advanced > Install app component 主命令将其安装到本地 Maven 仓库中。这个命令实际上只是在停止 Gradle 守护进程之后运行
install
Gradle 任务。
- 上传到远程 Maven 仓库
-
-
按照安装配置私仓中的说明设置私仓。
-
指定项目的仓库和凭证,由于使用私仓,此处不能设置标准 CUBA 仓库。
-
在文本编辑器中打开组件项目的
build.gradle
,并将uploadRepository
添加到cuba
部分:cuba { //... // repository for uploading your artifacts uploadRepository { url = 'http://repo.company.com/nexus/content/repositories/snapshots' user = 'admin' password = 'admin123' } }
-
在 Studio 中打开组件项目。
-
从命令行运行
uploadArchives
Gradle 任务。组件的工件将被上传到仓库。 -
从本地 Maven 仓库中删除组件工件,以确保在下一次装载应用程序项目时会从远程仓库下载组件:只需删除位于用户主目录中的
.m2/repository/com/company
文件夹。 -
现在,当装载并运行使用此组件的应用程序时,将从远程仓库中下载依赖的扩展组件。
-
- 上传到 Bintray
-
-
首先,在 https://bintray.com/signup/oss 注册账号
可以在 Bintray 上使用社交账户登录(GitHub,Gmail,Twitter),但稍后必须重置密码,因为获取 API 密钥需要此帐户的密码(见下文)。
-
获取 Bintray 用户名。可以在登录 Bintray 后看到的 URL 中找到。例如,在
https://bintray.com/vividphoenix
中,vividphoenix
是用户名。 -
获取 API 密钥。可以在 Bintray 编辑个人资料界面中找到。在 API 密钥部分,系统会要求输入帐户密码以获取密钥。然后,将能够使用此密钥和用户名进行 Bintray 的 Gradle 插件验证:
-
Bintray 凭证可以添加为环境变量:
BINTRAY_USER=your_bintray_user BINTRAY_API_KEY=9696c1cb90752357ded8fdf20eb3fa921bf9dbbb
-
除了环境变量,也可以在项目的
build.gradle
文件中显式定义这些参数:bintray { user = 'bintray_user' key = 'bintray_api_key' ... }
-
或者,可以在命令行中提供 Bintray 凭据的参数:
./gradlew clean assemble bintrayUpload -Pcuba.artifact.version=1.0.0 -PbintrayUser=your_bintray_user -PbintrayApiKey=9696c1cb90752357ded8fdf20eb3fa921bf9dbbb
-
-
创建 Maven 类型的公共仓库。对于开源(OSS)仓库,必须设置许可证类型。
Bintray 会隐式的使用仓库内的包。此时,还不是必须要创建组件包,因为在之后的
gradle bintrayUpload
任务会自动创建。 -
在
build.gradle
中,添加 Bintray 上传插件的依赖如下:buildscript { // ... dependencies { classpath "com.haulmont.gradle:cuba-plugin:$cubaVersion" // Bintray upload plugin classpath "com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.0" } }
-
在
build.gradle
结尾,添加 Bintray 插件设置:/** * 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 } } }
-
pkg:repo
是仓库(使用main
), -
pkg:name
是包名(使用唯一名称,例如amazingsearch
), -
pkg:desc
将显示在 Bintray 界面上的可选组件包描述, -
pkg:userOrg
- 是仓库所属组织的名称(如果没有设置,默认情况下BINTRAY_USER
将用作组织名称)。
-
-
现在,可以使用以下命令构建和上传项目:
./gradlew clean assemble bintrayUpload -Pcuba.artifact.version=1.0.0
-
如果在 CUBA 市场上发布扩展组件,则其仓库将链接到标准 CUBA 仓库,用户不必在其项目中指定仓库。
-
4.5.3. 应用程序组件示例
在本节中,我们将展示创建应用程序组件并在项目中使用的完整示例。该组件将提供 "客户管理(Customer Management)"功能,并包括 客户(Customer)
实体和相应的 UI 界面。应用程序将使用组件中的 Customer
实体作为其 订单(Order)
实体中的引用。
- 创建客户管理组件
-
-
在 Studio 中创建一个新项目,并在 New project 界面上指定以下参数:
-
Project name -
customers
-
Project namespace -
cust
-
Root package -
com.company.customers
-
-
打开 Project properties 窗口,在 Main 标签页上将 Module prefix 设置为
cust
。 -
创建至少有
name
属性的Customer
实体。在 Instance name 字段,设置 name pattern 属性。如果组件包含
@MappedSuperclass
持久化类,请确保它们在同一个项目中有后代实体(即使用@Entity
注解)。否则,这些基类将无法被正确增强(enhanced),并无法在应用程序中使用它们。 -
生成 DB 脚本并为
Customer
实体创建标准界面:cust_Customer.browse
和cust_Customer.edit
。 -
切换到菜单编辑器(menu designer),将
application-cust
菜单项更名为customerManagement
。然后,打开 Main Message Pack 部分的messages.properties
,为新的customerManagement
设置标题。 -
通过点击主菜单 CUBA > Advanced > App Component Descriptor 生成 组件描述文件
app-component.xml
。 -
测试客户管理功能:
-
主菜单选择 CUBA > Create Database。
-
启动应用程序:点击主工具栏
CUBA Application
配置旁边的调试按钮。 -
浏览器打开
http://localhost:8080/cust
。
-
-
通过点击 CUBA > Advanced > Install App Component 菜单项将应用程序组件安装到本地 Maven 仓库中。
-
- 创建 Sales 应用程序
-
-
在 Studio 中创建一个新项目,并在 New project 界面上指定以下参数:
-
Project name -
sales
-
Project namespace -
sales
-
Root package -
com.company.sales
-
-
打开 Project properties 窗口,并在 Repositories 部分中将本地 Maven 仓库添加到仓库列表。
-
在 App components 面板上单击 Custom components 旁边的加号按钮。在 Custom application component 对话框中,输入客户管理组件的 Maven 坐标,比如,
com.company.customers:cust-global:0.1-SNAPSHOT
。点击 OK 按钮保存项目属性设置。如果 Studio 建议更改 Gradle 脚本,同意即可。 -
创建
Order
实体并添加date
和amount
属性。然后添加customer
属性,与Customer
实体多对一关联 - Customer 在 Type 下拉列表中可用。 -
生成 DB 脚本并为
Order
实体创建标准界面。在创建标准界面时,先创建一个包含customer
属性的order-with-customer
视图,并将该视图用于界面展示。 -
测试应用程序功能:
-
在主菜单选择 CUBA > Create Database。
-
启动应用程序:点击主工具栏
CUBA Application
配置旁边的调试按钮。 -
浏览器打开
http://localhost:8080/cust
。应用程序将包含两个顶层菜单:Customer Management 和 Application,并都带有相应的功能。
-
-
- 修改客户管理组件
-
假设现在必须更改组件功能(在
Customer
中添加一个属性),然后重新装配应用程序以合并更改。-
在 Studio 中打开
customers
项目。 -
编辑
Customer
实体并添加address
属性。在浏览和编辑界面都需要包含此属性。 -
生成数据库脚本 - 将创建更新表的脚本。保存脚本。
-
测试组件中的更改:
-
在主菜单选择 CUBA > Update Database。
-
启动应用程序:点击主工具栏
CUBA Application
配置旁边的调试按钮。 -
浏览器打开
http://localhost:8080/cust
。
-
-
通过执行 CUBA > Advanced > Install App Component 菜单项将应用程序组件重新安装到本地 Maven 仓库中。
-
关闭 Studio 中的
sales
项目(如果已打开)并再次打开它。这是在 Studio 中下载新组件源代码所必须的。 -
点击 CUBA > Build Tasks > Clean,然后再点击 CUBA > Build Tasks > Assemble 菜单项。
-
点击主菜单 CUBA > Update Database - 会执行客户管理组件的更新脚本。
-
启动应用程序:点击主工具栏
CUBA Application
配置旁边的调试按钮。 -
浏览器打开
http://localhost:8080/app
应用程序会有包含新address
属性的Customer
实体以及界面。
-
4.5.4. 注册组件中的 DispatcherServlet
本节将介绍如何将一个应用程序组件中的 servlet 和 filter 配置传递到使用该组件的应用程序。为了避免web.xml文件中的代码重复,需要在组件中使用特殊的 ServletRegistrationManager
bean 注册 servlet 和 filter。
关于 Servlet 注册的最常见情况在示例HTTP servlet 注册中介绍。我们考虑一个更复杂的例子:一个应用程序组件带有一个用于处理 Web 请求的自定义 DispatcherServlet
的实现。
这个 servlet 从 demo-dispatcher-spring.xml
文件加载配置,如果要该 servlet 正常工作,应该在源码根目录(例如 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);
}
}
要注册 DispatcherServlet
,必须手动对此类进行加载、实例化、初始化,否则不同的类加载器可能会在 SingleWAR/SingleUberJAR 部署的情况下引发问题。而且,自定义 DispatcherServlet
应该需要进行双重初始化 - 第一次手动初始化,第二次由 servlet 容器初始化。
下面是一个初始化 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/*");
}
}
注入的 ServletRegistrationManager
bean 的 createServlet()
方法从 ServletContextInitializedEvent
获取应用程序上下文,并获取 WebDispatcherServlet
类的完全限定名。要初始化 servlet,需要传递从 ServletContextInitializedEvent
获得的 ServletContext
实例和 servlet 名称。addMapping()
方法用于定义通过 URL:/webd/
访问 servlet 的 HTTP 映射。
4.6. 日志
平台使用 Logback 框架来处理日志。
日志的输出,采用 SLF4J API:取到当前类的一个 logger,然后调用 logger 的方法打印日志,示例:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Foo {
// create logger
private Logger log = LoggerFactory.getLogger(Foo.class);
private void someMethod() {
// output message with DEBUG level
log.debug("someMethod invoked");
}
}
4.6.1. 在 Tomcat 里面配置日志
本章介绍在开发环境怎么配置日志。
执行 Gradle setupTomcat 任务会将 Tomcat 安装到项目目录并且添加 Tomcat 的其它配置。具体来说,会在 tomcat/bin
目录创建 setenv.bat
和 setenv.sh
环境配置文件,还有在 tomcat/conf
目录创建 logback.xml
文件。
除了所有的其它配置,setenv.*
还使用 CATALINA_OPTS
变量定义了 logback.xml
配置文件需要加载的参数。
logback.xml
定义了日志的配置。文件有如下结构:
-
appender
元素定义了日志的 “输出设备”。主要的 appenders 有FILE - 文件
和CONSOLE - 终端
。ThresholdFilter
的level
参数定义了消息的阈值。文件输出的默认值是DEBUG
,终端输出的默认值是INFO
。也就是说ERROR
,WARN
,INFO
和DEBUG
消息会输出到文件,但是只有ERROR
,WARN
和INFO
级别的消息输出到终端。file
参数定义了文件输出目标文件的路径。默认值是tomcat/logs/app.log
。 -
logger
元素定义了编码角度打印消息的 logger 参数。Logger 名称是有级别的,比如对于com.company.sample
logger 的设置也会影响com.company.sample.core.CustomerServiceBean
和com.company.sample.web.CustomerBrowse
的 logger,前提是这两个类没有显式的声明他们各自的 logger 参数。日志的打印级别是通过
level
属性来定义最低级别。比如,如果定义的是INFO
级别,那么DEBUG
和TRACE
类型的消息就不会被日志记录。需要注意的一点就是,在 appender 里面设置的级别也会影响日志的打印。Logger 是针对类或者包的日志定义,而 appender 是针对文件或者终端的设置。
可以在 web 客户端的 Administration > Server Log 界面快速修改正在运行的服务的 logger 级别和 appender 的阈值。任何对日志的改动只会影响正在运行的服务,设置并不会保存在文件里。这个界面也支持从日志目录 (tomcat/logs)查看和加载 Tomcat 服务的日志。
平台会自动添加以下信息到日志文件的消息中:
-
application – 打印消息的应用程序名称。这个信息可以帮助定位消息是从哪个 block 打印的(Middleware, Web Client),因为这两个模块写的是同一个日志文件。
-
user – 调用打印消息代码的登录用户名。用来在日志里跟踪具体用户的行为。如果打印消息的代码没有被特定用户的会话调用,就不会在日志里添加用户信息。
比如,下面这个消息是被 admin
会话下调用的中间件(app-core
)代码写入的:
16:12:20.498 DEBUG [http-nio-8080-exec-7/app-core/admin] com.haulmont.cuba.core.app.DataManagerBean - loadList: ...
4.6.2. 一些有用的 Logger 配置
以下是一些框架里面比较有用的 loggers,可以用来调试问题。
- eclipselink.sql
-
如果设置成
DEBUG
,EclipseLink ORM 框架会打印所有执行的 SQL 语句和执行时间。这个 logger 已经在标准的logback.xml
里面定义了,所以使用的时候只需要修改它的日志级别,示例:<configuration> ... <logger name="eclipselink.sql" level="DEBUG"/>
日志输出的样例:
2018-09-21 12:48:18.583 DEBUG [http-nio-8080-exec-5/app-core/admin] com.haulmont.cuba.core.app.RdbmsStore - loadList: metaClass=sec$User, view=com.haulmont.cuba.security.entity.User/user.browse, query=select u from sec$User u, max=50 2018-09-21 12:48:18.586 DEBUG [http-nio-8080-exec-5/app-core/admin] eclipselink.sql - <t 891235430, conn 1084868057> SELECT t1.ID AS a1, t1.ACTIVE AS a2, t1.CHANGE_PASSWORD_AT_LOGON AS a3, t1.CREATE_TS AS a4, t1.CREATED_BY AS a5, t1.DELETE_TS AS a6, t1.DELETED_BY AS a7, t1.EMAIL AS a8, t1.FIRST_NAME AS a9, t1.IP_MASK AS a10, t1.LANGUAGE_ AS a11, t1.LAST_NAME AS a12, t1.LOGIN AS a13, t1.LOGIN_LC AS a14, t1.MIDDLE_NAME AS a15, t1.NAME AS a16, t1.PASSWORD AS a17, t1.POSITION_ AS a18, t1.TIME_ZONE AS a19, t1.TIME_ZONE_AUTO AS a20, t1.UPDATE_TS AS a21, t1.UPDATED_BY AS a22, t1.VERSION AS a23, t1.GROUP_ID AS a24, t0.ID AS a25, t0.DELETE_TS AS a26, t0.DELETED_BY AS a27, t0.NAME AS a28, t0.VERSION AS a29 FROM SEC_USER t1 LEFT OUTER JOIN SEC_GROUP t0 ON (t0.ID = t1.GROUP_ID) WHERE (t1.DELETE_TS IS NULL) LIMIT ? OFFSET ? bind => [50, 0] 2018-09-21 12:48:18.587 DEBUG [http-nio-8080-exec-5/app-core/admin] eclipselink.sql - <t 891235430, conn 1084868057> [1 ms] spent
- com.haulmont.cuba.core.sys.AbstractWebAppContextLoader
-
如果设置成
TRACE
,框架会在服务启动时打印文件中定义的应用程序属性和程序模块中定义的属性,可以用来调试启动时候出现的问题。需要注意的是,应该给一个合适的 appender 的日志级别设置成
TRACE
,因为通常 appenders 的日志级别都设定的比较高。比如:<configuration> ... <appender name="File" class="ch.qos.logback.core.rolling.RollingFileAppender"> <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>TRACE</level> </filter> ... <logger name="com.haulmont.cuba.core.sys.AbstractWebAppContextLoader" level="TRACE"/>
日志输出的样例:
2018-09-21 12:38:59.525 TRACE [localhost-startStop-1] com.haulmont.cuba.core.sys.AbstractWebAppContextLoader - AppProperties of the 'core' block: cuba.anonymousSessionId=9c91dbdf-3e73-428e-9088-d586da2434c5 cuba.automaticDatabaseUpdate=true ...
4.7. 调试
本章节介绍使用 CUBA 应用程序调试的步骤。
4.7.1. 连接调试器
可以通过两种方式启动 Tomcat 服务的调试模式。一种是通过 Gradle 任务 :
gradlew start
另一种是通过运行安装的 Tomcat 的 bin/debug.*
文件。
启动之后,应用服务可以从 8787 端口接收调试器的连接。端口号可以在 bin/setenv.*
文件中的 JPDA_OPTS
变量修改。
如果使用 Intellij IDEA 调试,需要创建一个 Remote 类型的 Run/Debug Configuration 元素,并且设置调试连接的 Port 为 8787(默认值)。
4.7.2. 调试 Widgetset 版本
不使用 GWT Super Dev Mode 在客户端最容易调试应用程序的方法就是使用 web 模块设置里面的调试配置(configuration)。
-
在
webModule
中添加新调试配置:configure(webModule) { configurations { webcontent debug // a new configuration } '''''' }
-
在
webModule
里的dependencies
部分添加调试的依赖:dependencies { provided(servletApi) compile(guiModule) debug("com.haulmont.cuba:cuba-web-toolkit:$cubaVersion:debug@zip") }
如果使用了 charts 组件,那么必须添加
debug("com.haulmont.charts:charts-web-toolkit:$cubaVersion:debug@zip")
。 -
在
webModule
的配置部分添加deploy.doLast
任务:task deploy.doLast { project.delete "$cuba.tomcat.dir/webapps/app/VAADIN/widgetsets" project.copy { from zipTree(configurations.debug.singleFile) into "$cuba.tomcat.dir/webapps/app" } }
调试场景会被部署在项目的 $cuba.tomcat.dir/webapps/app/VAADIN/widgetsets/com.haulmont.cuba.web.toolkit.ui.WidgetSet
目录。
4.7.3. 调试 web Widgets
可以在浏览器使用 GWT Super Dev Mode - GWT 超级开发模式 来调试 web widgets。
-
在
build.gradle
里设置 debugWidgetSet 任务。 -
部署应用程序并启动 Tomcat。
-
执行
debugWidgetSet
任务:gradlew debugWidgetSet
运行中的 GWT 代码服务会在修改代码的时候自动重编译。
-
在 Chrome 浏览器打开
http://localhost:8080/app?debug&superdevmode
然后等待 widgetset 第一次构建。 -
在 Chrome 打开调试控制器窗口:
-
在
web-toolkit
模块修改了 Java 代码之后,刷新浏览器页面。Widgetset 会重新增量构建,大概需要 8-10 秒时间。
4.8. 测试
本章节介绍 CUBA 应用程序在不同层(layers)的测试方法。
4.8.1. 单元测试
假设有下面这个界面控制器:
public class OrderEditor extends AbstractEditor {
@Named("itemsTable.add")
protected AddAction addAction;
@Override
public void init(Map<String, Object> params) {
addAction.setWindowId("sales$Product.browse");
addAction.setHandler(new Lookup.Handler() {
@Override
public void handleLookup(Collection items) {
// some code
}
});
}
}
可以编写以下测试类来检查 init()
方法:
public class OrderEditorTest {
OrderEditor editor;
@Mocked
Window.Editor frame;
@Mocked
AddAction addAction;
@Before
public void setUp() throws Exception {
editor = new OrderEditor();
editor.setWrappedFrame(frame);
editor.addAction = addAction;
}
@Test
public void testInit() {
editor.init(Collections.<String, Object>emptyMap());
editor.setItem(new Order());
new Verifications() {
{
addAction.setWindowId("sales$Product.browse");
addAction.setHandler(withInstanceOf(Window.Lookup.Handler.class));
}
};
}
}
4.8.2. 中间件集成测试
在中间件层,可以创建运行在具有完整功能的 Spring 容器里的测试用例,而且可以连接数据库。在这些测试类里面,可以运行中间件里面各细分层的代码,比如从 ORM 层到 Service 层。
首先,在 core 模块跟 src
目录平级创建 test
目录。然后在 Studio 里面重新创建 IDE 项目文件,以便在 IDE 里面运行测试。
平台自有的 TestContainer
类可以用来作为项目里面测试容器的基类。在 core 模块的 test
目录创建一个 TestContainer
类的子类,然后在这个子类的构造方法里面,重新定义加载 应用程序组件和 应用程序属性以及测试数据库连接的参数,示例:
public class SalesTestContainer extends TestContainer {
public SalesTestContainer() {
super();
appComponents = Arrays.asList(
"com.haulmont.cuba"
// add CUBA premium add-ons here
// "com.haulmont.bpm",
// "com.haulmont.charts",
// "com.haulmont.fts",
// "com.haulmont.reports",
// and custom app components if any
);
appPropertiesFiles = Arrays.asList(
// List the files defined in your web.xml
// in appPropertiesConfig context parameter of the core module
"com/company/sales/app.properties",
// Add this file which is located in CUBA and defines some properties
// specifically for test environment. You can replace it with your own
// or add another one in the end.
"test-app.properties",
"com/company/sales/test-app.properties");
dbDriver = "org.postgresql.Driver";
dbUrl = "jdbc:postgresql://localhost/sales_test";
dbUser = "cuba";
dbPassword = "cuba";
}
}
自定义 test-app.properties
文件的示例:
cuba.webContextName = app-core
sales.someProperty = someValue
推荐使用单独的测试数据库,可以通过 build.gradle
里面定义的 createDb 任务来创建:
configure(coreModule) {
...
task createTestDb(dependsOn: assemble, description: 'Creates local Postgres database for tests', type: CubaDbCreation) {
dbms = 'postgres'
dbName = 'sales_test'
dbUser = 'cuba'
dbPassword = 'cuba'
}
这个测试容器应当在测试类里面作为 @ClassRule
注解指定的 JUnit 规则(rule):
public class CustomerLoadTest {
@ClassRule
public static SalesTestContainer cont = SalesTestContainer.Common.INSTANCE;
private Customer customer;
@Before
public void setUp() throws Exception {
customer = cont.persistence().createTransaction().execute(em -> {
Customer customer = cont.metadata().create(Customer.class);
customer.setName("testCustomer");
em.persist(customer);
return customer;
});
}
@After
public void tearDown() throws Exception {
cont.deleteRecord(customer);
}
@Test
public void test() {
try (Transaction tx = cont.persistence().createTransaction()) {
EntityManager em = cont.persistence().getEntityManager();
TypedQuery<Customer> query = em.createQuery(
"select c from sales$Customer c", Customer.class);
List<Customer> list = query.getResultList();
tx.commit();
assertTrue(list.size() > 0);
}
}
}
在上面的例子中,测试容器只初始化了一次,但是可以给这个类所有的测试方法使用,等他们执行完之后就销毁了。
容器的初始化需要一些时间,有没有办法只创建一个测试容器但是给好几个测试类使用呢?可以通过创建一个通用的测试容器单例:
public class SalesTestContainer extends TestContainer {
public SalesTestContainer() {
...
}
public static class Common extends SalesTestContainer {
public static final SalesTestContainer.Common INSTANCE = new SalesTestContainer.Common();
private static volatile boolean initialized;
private Common() {
}
@Override
public void before() throws Throwable {
if (!initialized) {
super.before();
initialized = true;
}
setupContext();
}
@Override
public void after() {
cleanupContext();
// never stops - do not call super
}
}
}
在测试类里使用:
public class CustomerLoadTest {
@ClassRule
public static SalesTestContainer cont = SalesTestContainer.Common.INSTANCE;
...
}
- 几个有用的测试容器方法
-
TestContainer
类包含了以下几个方法,可以在测试类里面使用(参考上面的CustomerLoadTest
例子):-
persistence()
– 返回 Persistence 接口的引用。 -
metadata()
– 返回 Metadata 接口的引用。 -
deleteRecord()
– 这一组重载方法的目的是在@After
方法里面使用,在测试完成后清理数据库。
-
- 日志
-
测试容器根据平台提供的
test-logback.xml
文件来配置日志。这个文件在cuba-core-tests
工件的根目录。可以通过以下方法配置测试的日志级别:
-
从平台的包里面拷贝
test-logback.xml
到项目core
模块test
根目录下,比如可以重命名为my-test-logback.xml
。 -
在
my-test-logback.xml
里面配置 appenders 和 loggers。 -
在测试容器里面添加一段静态初始化代码,这段代码通过设置
logback.configurationFile
这个系统属性来指定日志配置文件的位置:public class MyTestContainer extends TestContainer { static { System.setProperty("logback.configurationFile", "my-test-logback.xml"); } // ... }
-
- 额外数据存储
-
如果项目使用了额外数据存储,需要在测试容器里创建相应的 JDBC 数据源。比如,如果有名为
mydb
的数据存储,而且是 PostgreSQL 的数据库,则需要在测试容器中添加如下代码:public class MyTestContainer extends TestContainer { // ... @Override protected void initDataSources() { super.initDataSources(); try { Class.forName("org.postgresql.Driver"); TestDataSource mydbDataSource = new TestDataSource( "jdbc:postgresql://localhost/mydatabase", "db_user", "db_password"); TestContext.getInstance().bind( AppContext.getProperty("cuba.dataSourceJndiName_mydb"), mydbDataSource); } catch (ClassNotFoundException | NamingException e) { throw new RuntimeException("Error initializing datasource", e); } } }
还有,如果额外的数据库类型跟主数据库不一致,需要在
build.gradle
的core
模块将数据库的驱动添加到testRuntime
依赖中。示例:configure(coreModule) { // ... dependencies { // ... testRuntime(hsql) jdbc('org.postgresql:postgresql:9.4.1212') testRuntime('org.postgresql:postgresql:9.4.1212') // add this }
4.9. 热部署
CUBA 框架支持热部署技术,可以在项目运行时进行项目改动的部署,改动即时生效而且不需要重启应用服务。本质上,热部署是将项目的资源改动和 Java 源文件改动拷贝到应用的配置目录,然后运行中的应用程序会编译源文件并且加载新的类和资源。
- 工作原理
-
当项目中的源代码改动时,Studio 会拷贝改动过的文件到 web 应用程序的配置目录 (
tomcat/conf/app
或者tomcat/conf/app-core
)。由于 Tomcat 配置目录中资源的优先级比应用程序 JAR 包里面的高,所以程序会在下次需要这些资源的时候从配置目录加载。如果需要加载的是 Java 源码,则会先编译再加载编译过后的类。Studio 也会给应用程序发信号,通知它清理掉缓存以便加载改动过的资源,这些缓存包含信息(messages)缓存、view 的配置、注册的界面还有菜单。
当应用程序服务重启的时候,所有在配置目录的文件都会被删除,因为新的 JAR 包会包含代码的最新改动。
- 能做热部署的部分
- 不能热部署的部分
- 在 Studio 里面使用热部署
-
热部署的设置可以在 Studio 中进行配置:主菜单点击 CUBA > Settings,然后选择 CUBA > Project settings 元素。
-
点击 Hot Deploy Settings 链接可以配置源代码路径和 Tomcat 路径的映射关系。
-
Instant hot deploy 复选框可以设置关闭当前项目的热部署
当热部署禁用之后,可以通过在主菜单点击 CUBA > Build Tasks > Hot Deploy To Configuration Directory 手动触发。
-
5. 应用程序部署
本章节介绍了 CUBA 应用程序部署和操作的不同概念。
下图是一个可能的部署架构。这个架构消除了单点故障,提供了负载均衡和不同客户端的连接。
最简单的情况,应用程序可以安装在一台机器,并且包含了数据库。根据负载和容错性要求可以选择多样的部署场景,细节请参考 应用程序扩展。
5.1. 应用程序主目录
应用程序主目录即是所有应用程序目录的根目录。通常在 WAR 或者 UberJAR 包中的 /WEB-INF/local.app.properties
文件指定。
-
如果构建一个 WAR 包,必须在 buildWar Gradle 任务中定义应用程序主目录的路径。可以设置为绝对路径或者相对路径,如果是相对路径的话,需要提前知道 WAR 将会被部署在服务器的哪个工作目录。如果不知道的话,可以用 Java 系统变量的占位符,然后在运行时提供真实路径。
在运行时设置应用程序主目录的示例:
-
任务配置:
task buildWar(type: CubaWarBuilding) { appHome = '${app.home}' // ... }
-
构建了 WAR 之后
/WEB-INF/local.app.properties
文件的内容:cuba.logDir = ${app.home}/logs cuba.confDir = ${app.home}/${cuba.webContextName}/conf cuba.tempDir = ${app.home}/${cuba.webContextName}/temp cuba.dataDir = ${app.home}/${cuba.webContextName}/work ...
-
命令行传递
app.home
系统参数:java -Dapp.home=/opt/app_home ...
设置 Java 系统参数的方式取决于应用程序服务。对于 Tomcat,推荐设置在
bin/setenv.sh
(或者bin/setenv.bat
)文件内。 -
生成的目录结构:
/opt/app_home/ app/ conf/ temp/ work/ app-core/ conf/ temp/ work/ logs/
-
-
对于 UberJAR 的情况,应用程序主目录默认设置为工作目录,但是也可以通过 Java 系统参数
app.home
来重定义。所以如果需要跟上面提到的 WAR 的例子一样设定同样的主目录,可以通过命令行执行:java -Dapp.home=/opt/app_home -jar app.jar
5.2. 应用程序文件目录
本章节介绍应用程序中各个 blocks 在运行时使用到的系统文件目录。
5.2.1. 配置文件目录
配置文件目录包含一些资源,这些资源可以在应用程序部署之后对项目配置、用户界面或者业务逻辑进行补充或者重写。重写是由 Resources 基础设施接口加载机制提供的,此机制先在配置文件目录进行搜索,然后才是 classpath,因此配置文件目录资源会比 JAR 文件和 classpath 里面同名资源的优先级要高。
配置文件目录可能包含以下类型的资源:
-
配置文件:metadata.xml,persistence.xml,views.xml,remoting-spring.xml。
-
UI 界面的 XML 描述
-
Java 或者 Groovy 源代码形式的界面控制器。
-
Groovy 脚本、类,或者 Java 源代码 - 程序会通过 Scripting 接口使用
配置文件目录通过 cuba.confDir 应用程序属性指定。在 Tomcat 的快速部署模式下,这个目录是 tomcat/conf
下面的一个用应用程序名字命名的目录,比如中间件的 tomcat/conf/app-core
目录。如果是其它的部署方式,这个目录会在应用程序主目录里。
5.2.2. 工作目录
应用程序使用工作目录来存储一些持久化数据和配置。
比如,文件存储机制默认使用工作目录的 filestorage
子目录。还有,中间件模块会将启动时自动生成的 persistence.xml 和 orm.xml
保存到工作目录。
工作目录是由 cuba.dataDir 应用程序属性指定。在 Tomcat 的快速部署模式下,这个目录是 tomcat/work
下面的一个用应用程序名字命名的目录。如果是其它的部署方式,这个目录会在应用程序主目录里。
5.2.3. 日志目录
日志文件的内容是由 Logback 框架的配置决定的。平台在 classpath 的根目录提供了默认的配置文件 logback.xml
。根据这个文件的设定,日志信息会在标准输出打印。
如果要做自定义的日志配置,可以通过 Java 系统属性 logback.configurationFile 来指向自定义的配置文件。参考 Tomcat 日志配置了解如何在快速部署中进行配置。
日志配置决定了日志文件的位置。可以是 Tomcat 下一个特定的目录(快速部署下的 tomcat/logs
目录),或者是应用程序主目录的一个目录。可以通过修改项目目录下 deploy/tomcat/conf
里面的 logback.xml
来控制日志目录,修改 logDir
属性即可,示例:
<configuration debug="false">
<property name="logDir" value="${app.home}/logs"/>
<!-- ... -->
应用程序需要知道日志文件存放在哪里,以便系统管理员通过 Administration > Server Log 界面加载和查看日志。所以,需要设置 cuba.logDir 这个应用程序属性与 logback.xml
里面配置的目录一致。
参考日志。
5.2.4. 临时目录
应用程序会在运行时使用这个目录来存放一些临时文件。通过 cuba.tempDir 应用程序属性指定这个目录。在 Tomcat 的快速部署模式下,这个目录是 tomcat/temp
下面的一个用应用程序名字命名的目录。如果是其它的部署方式,这个目录会在应用程序主目录里。
5.2.5. 数据库脚本目录
这个目录存放了用来创建和更新数据库的 SQL 脚本。是中间件 block 独有的目录。
脚本目录结构是按照 创建和更新数据库的脚本 里面描述的构建的,但是这些目录还有额外的更高一级的目录,用来区分应用程序组件和应用程序本身的脚本。高一级目录的数字编号是项目构建 任务创建的。
数据库脚本目录是由 cuba.dbDir 应用程序属性指定。在 Tomcat 的快速部署模式下,这个目录是中间件 web 应用程序目录下的 WEB-INF/db
目录,比如:tomcat/webapps/app-core/WEB-INF/db
。如果是其它的部署方式,这个目录会 WAR 或者 UberJAR 文件里的 /WEB-INF/db
目录。
5.3. 部署选项
本章节介绍了部署 CUBA 应用程序的不同方法。
5.3.1. Tomcat 快速部署
快速部署在开发程序的时候是默认的部署方式,因为快速部署的构建用时最少,能自动安装并且启动应用服务。生产环境也可以用这种方式部署。
快速部署是使用 build.gradle
文件中的 core 和 web 模块的 deploy 任务。在第一次执行 deploy
之前,应当先执行 setupTomcat 任务来安装并且初始化本地 Tomcat 服务。
需要确保系统环境没有 |
快速部署之后,会在 Tomcat 目录下创建以下目录结构(只列出重要的目录和文件),Tomcat 目录是由 build.gradle
脚本的 cuba.tomcat.dir
参数指定:
bin/
setenv.bat, setenv.sh
startup.bat, startup.sh
debug.bat, debug.sh
shutdown.bat, shutdown.sh
conf/
catalina.properties
server.xml
logback.xml
logging.properties
Catalina/
localhost/
app/
app-core/
lib/
hsqldb-2.2.9.jar
logs/
app.log
shared/
lib/
temp/
app/
app-core/
webapps/
app/
app-core/
work/
app/
app-core/
-
bin
– 包含配置、启动和停止 Tomcat 服务的工具:-
setenv.bat
,setenv.sh
– 这两个脚本用来设置环境变量。这些脚本可以用来设置 JVM 内存参数、logging 日志的配置文件、配置访问 JMX,以及连接调试器的参数。如果在 Linux 虚拟机(VPS)中,Tomcat 启动的很慢,可以尝试在
setenv.sh
里给 JVM 配置非阻塞熵源(non-blocking entropy source):CATALINA_OPTS="$CATALINA_OPTS -Djava.security.egd=file:/dev/./urandom"
-
startup.bat
,startup.sh
– 启动 Tomcat 服务的脚本。在 Windows 环境,Tomcat 会在一个单独的终端窗口启动,但是在 *nix 系统服务会在后台启动。需要在当前终端窗口启动服务,使用以下命令代替
startup.*
:> catalina.bat run
$ ./catalina.sh run
-
debug.bat
,debug.sh
– 跟startup.*
类似,但是会启动能连接调试器的 Tomcat 服务。这些脚本会在执行构建脚本中的 start 任务的时候使用。 -
shutdown.bat
,shutdown.sh
– 停止 Tomcat 服务。
-
-
conf
– 包含 Tomcat 的配置文件和部署的应用程序。-
catalina.properties
– Tomcat 属性文件。如果需要从shared/lib
目录加载共享库(参阅后面),这个文件需要配置下面这行:shared.loader=${catalina.home}/shared/lib/*.jar
-
server.xml
– Tomcat 配置描述文件。 -
logback.xml
– 应用程序日志配置描述文件。 -
logging.properties
– Tomcat 服务日志配置描述文件。 -
Catalina/localhost
– 这个目录下可以放置 context.xml 应用程序部署描述文件。这个目录下放置的描述文件会比META-INF
目录下的描述文件优先级高。这种机制可以用在生产环境。比如,通过这种机制可以配置跟应用程序本身不同的数据库连接参数,从而达到生产环境连接不同数据库的要求。针对不同服务的描述文件需要有不同服务的应用程序名称和
.xml
扩展名。所以,如果是为app-core
创建部署描述文件,需要拷贝webapps/app-core/META-INF/context.xml
文件成conf/Catalina/localhost/app-core.xml
文件,然后通过修改conf/Catalina/localhost/app-core.xml
内容覆盖设置。 -
app
– web 客户端应用程序配置目录。 -
app-core
– 中间件应用程序配置目录。
-
-
lib
– 服务的 通用类加载器(common classloader) 加载类库的目录。这些类库可以被这个 Tomcat 服务和所有部署在其中的 web 应用程序加载。还有,这个目录应该有数据库的 JDBC 驱动(hsqldb-XYZ.jar
,postgresql-XYZ.jar
等)。 -
logs
– 应用程序和 Tomcat 服务的日志目录。应用程序的主要日志文件是app.log
(参考为 Tomcat 设置日志)。 -
shared/lib
– 所有部署的应用可访问的类库目录。服务的 共享类加载器(shared classloader) 会加载这些类库。使用这个目录的方法在上面conf/catalina.properties
文件中提到过。构建脚本的 deploy 任务会拷贝所有不在
jarNames
参数列举的类库到这个目录。 -
temp/app
,temp/app-core
– web 客户端和中间件应用程序的临时目录。 -
webapps
– web 应用程序目录。每个应用程序在它自己的子目录里,子目录按照 展开成文件夹的 WAR(exploded WAR) 形式命名。构建脚本的 deploy 任务会按照
appName
参数来创建应用程序子目录,除了其它的文件之外,还会拷贝列在jarNames
参数的类库到每个应用程序的WEB-INF/lib
目录。 -
work/app
,work/app-core
– web 客户端和中间件应用的工作目录。
5.3.1.1. 生产环境使用 Tomcat
默认情况下,快速部署过程会在本地 Tomcat 实例下创建 app
和 app-core
这两个 web 服务,并且运行在 8080 端口。也就是说 web 客户端可以通过 http://localhost:8080/app
访问。
这个 Tomcat 可以作为生产环境使用,只需要拷贝 tomcat
目录到生产环境服务器,只需要修改 conf/app/local.app.properties
和 conf/app-core/local.app.properties
里面的服务器名称,这两个文件如果没有的话,需要创建:
cuba.webHostName = myserver
cuba.webAppUrl = http://myserver:8080/app
另外,如果开发的时候使用的是测试库,那么需要在生产环境修改数据库连接以便使用生产库。可以通过修改 web 应用程序的context.xml (webapps/app-core/META-INF/context.xml
)文件。也可以按照前面章节介绍的拷贝这个文件为 conf/Catalina/localhost/app-core.xml
,这样的话可以分别使用独立的开发和测试库环境配置。
可以从开发库备份来创建生产库,或者可以配置自动创建和更新生产库,参考生产环境更新数据库。
- 可选配置
-
-
如果需要更改 Tomcat 端口或者 web 上下文(也就是 URL 里面
/
最后的那部分),可通过 Studio 进行配置:-
在 Studio 中打开项目。
-
转到 Project Properties 。
-
需要更改 web 上下文,可以编辑 Modules prefix 字段
-
需要改变 Tomcat 端口,可以编辑 Tomcat ports > HTTP port 字段
-
-
如果希望使用根节点上下文(
http://myserver:8080/
),可以将 Modules prefix 直接设置成ROOT
tomcat/ conf/ ROOT/ local.app.properties app-core/ local.app.properties webapps/ ROOT/ app-core/
然后在
conf/ROOT/local.app.properties
文件里,使用/
作为 web 上下文名称:cuba.webContextName = /
-
5.3.2. 部署 WAR 至 Jetty
以下是一个部署 WAR 包到 Jetty web 服务器的示例,假设应用程序使用的是 PostgreSQL 数据库。
-
使用 Studio 中的 Deployment > WAR settings 界面或者手动在 build.gradle 末尾添加 buildWar 任务。
task buildWar(type: CubaWarBuilding) { appHome = '${app.home}' appProperties = ['cuba.automaticDatabaseUpdate': 'true'] singleWar = false }
需要注意的是,这里给 Middleware 和 web 客户端构建了单独的两个 WAR 文件。
-
从命令行启动
buildWar
任务(假设已经预先创建了 Gradle wrapper):gradlew buildWar
如果成功的话,会在项目的
build\distributions\war
目录创建app-core.war
和app.war
。 -
创建一个应用程序主目录目录,比如,
c:\work\app_home
。 -
下载并安装 Jetty 到本地目录,比如
c:\work\jetty-home
。本示例使用jetty-distribution-9.3.6.v20151106.zip
测试通过。 -
创建
c:\work\jetty-base
目录,并且在这个目录打开命令行窗口执行以下命令:java -jar c:\work\jetty-home\start.jar --add-to-start=http,jndi,deploy,plus,ext,resources
-
创建
c:\work\jetty-base\app-jetty.xml
文件,添加以下内容(假设使用名为test
PostgreSQL 数据库):<?xml version="1.0"?> <!DOCTYPE Configure PUBLIC "-" "http://www.eclipse.org/jetty/configure_9_0.dtd"> <Configure id="Server" class="org.eclipse.jetty.server.Server"> <New id="CubaDS" class="org.eclipse.jetty.plus.jndi.Resource"> <Arg></Arg> <Arg>jdbc/CubaDS</Arg> <Arg> <New class="org.postgresql.ds.PGSimpleDataSource"> <Set name="ServerName">localhost</Set> <Set name="PortNumber">5432</Set> <Set name="DatabaseName">test</Set> <Set name="User">cuba</Set> <Set name="Password">cuba</Set> </New> </Arg> </New> </Configure>
MS SQL 数据库的
app-jetty.xml
文件需要使用下面这个模板:<?xml version="1.0"?> <!DOCTYPE Configure PUBLIC "-" "http://www.eclipse.org/jetty/configure_9_0.dtd"> <Configure id='wac' class="org.eclipse.jetty.webapp.WebAppContext"> <New id="CubaDS" class="org.eclipse.jetty.plus.jndi.Resource"> <Arg/> <Arg>jdbc/CubaDS</Arg> <Arg> <New class="org.apache.commons.dbcp2.BasicDataSource"> <Set name="driverClassName">com.microsoft.sqlserver.jdbc.SQLServerDriver</Set> <Set name="url">jdbc:sqlserver://server_name;databaseName=db_name</Set> <Set name="username">username</Set> <Set name="password">password</Set> <Set name="maxIdle">2</Set> <Set name="maxTotal">20</Set> <Set name="maxWaitMillis">5000</Set> </New> </Arg> </New> </Configure>
-
或许(比如对于 MS SQL 数据库),需要下载以下这些 JAR 并且添加到
c:\work\jetty-base\lib\ext
目录。commons-pool2-2.4.2.jar commons-dbcp2-2.1.1.jar commons-logging-1.2.jar
-
将下面这些添加到
c:\work\jetty-base\start.ini
文件的开头:--exec -Xdebug -agentlib:jdwp=transport=dt_socket,address=8787,server=y,suspend=n -Dapp.home=c:\work\app_home -Dlogback.configurationFile=c:\work\app_home\logback.xml # --------------------------------------- app-jetty.xml
-
拷贝数据库的 JDBC 驱动到
c:\work\jetty-base\lib\ext
目录。可以从 CUBA Studio 的lib
目录或者项目的build\tomcat\lib
目录拷贝这些驱动。比如对于 PostgreSQL,驱动文件是postgresql-9.1-901.jdbc4.jar
。 -
拷贝 WAR 文件到
c:\work\jetty-base\webapps
目录。 -
在
c:\work\jetty-base
目录打开命令行窗口并且执行:java -jar c:\work\jetty-home\start.jar
-
在浏览器打开
http://localhost:8080/app
。
5.3.3. 部署 WAR 到 WildFly
CUBA 应用程序的 WAR 包可以部署在 WildFly 应用服务中。下面这个示例是部署使用 PostgreSQL 9.6 的 CUBA 应用程序到 Windows 的 WildFly 14.0.0。
-
编辑
build.gradle
并在 global 模块的dependencies
部分添加依赖:runtime 'org.reactivestreams:reactive-streams:1.0.1'
-
组装并且部署项目到默认的 Tomcat 服务,以便加载项目所有的依赖到本地。
-
为程序配置应用程序主目录:
-
创建一个 WildFly 服务能完全控制的目录,比如
C:\Users\UserName\app_home
。 -
从
tomcat/conf
拷贝logback.xml
文件到这个目录,并修改logDir
属性:
<property name="logDir" value="${app.home}/logs"/>
-
-
配置 WildFly 服务
-
在本地目录安装 WildFly,比如
C:\wildfly
。 -
编辑
C:\wildfly\bin\standalone.conf.bat
文件,并将下面这行添加到文件末尾:
set "JAVA_OPTS=%JAVA_OPTS% -Dapp.home=%USERPROFILE%/app_home -Dlogback.configurationFile=%USERPROFILE%/app_home/logback.xml"
这行里将
app.home
系统属性指向之前创建的应用程序主目录,并且设置日志的配置文件指向先前创建的logback.xml
文件。也可以使用绝对路径替换%USERPROFILE%
变量。-
对比一下 WildFly 的 Hibernate Validator 版本跟 CUBA 的有没有不同,如果 CUBA 使用的是比较新的版本,那么用
tomcat\shared\lib
下面的版本(比如hibernate-validator-5.4.2.Final.jar
)替换掉C:\wildfly\modules\system\layers\base\org\hibernate\validator\main\hibernate-validator-x.y.z-sometext.jar
。 -
在
\wildfly\modules\system\layers\base\org\hibernate\validator\main\module.xml
文件中更新一下替换的 JAR 包的版本。 -
在 WildFly 中注册 PostgreSQL 驱动,从
tomcat\lib
目录拷贝postgresql-9.4.1212.jar
到C:\wildfly\standalone\deployments
目录。 -
配置 WildFly logger:打开
\wildfly\standalone\configuration\standalone.xml
文件,在<subsystem xmlns="urn:jboss:domain:logging:{version}"
区域添加下面两行:<subsystem xmlns="urn:jboss:domain:logging:6.0"> <add-logging-api-dependencies value="false"/> <use-deployment-logging-config value="false"/> . . . </subsystem>
-
-
创建 JDBC 数据源
-
执行
standalone.bat
启动 WildFly。 -
浏览器打开管理员窗口
http://localhost:9990
。第一次登录的时候,需要创建用户名和密码。 -
打开 Configuration - Subsystems - Datasources and Drivers - Datasources 标签页,为应用程序创建新数据源:
Name: Cuba JNDI Name: java:/jdbc/CubaDS JDBC Driver: postgresql-9.4.1212.jar Driver Module Name: org.postgresql Driver Class Name: org.postgresql.Driver Connection URL: your database URL Username: your database username Password: your database password
如果按照之前的步骤拷贝了
postgresql-x.y.z.jar
,那么 JDBC 驱动会在自动检测到的驱动列表里显示。点击 Test connection 按钮测试数据库连接。
-
启用这个数据源。
-
-
构建应用程序
-
在 Studio 打开 Deployment > *WAR settings。
-
勾选 Build WAR 复选框。
-
在 Application home directory 字段设置
${app.home}
。 -
保存设置。
-
在 IDE 中打开 build.gradle 并且在 buildWar 任务中添加
doAfter
属性。这个属性会控制拷贝 WildFly 部署描述文件:task buildWar(type: CubaWarBuilding) { appProperties = ['cuba.automaticDatabaseUpdate' : true] singleWar = false appHome = '${app.home}' doAfter = { copy { from 'jboss-deployment-structure.xml' into "${project.buildDir}/buildWar/core/war/META-INF/" } copy { from 'jboss-deployment-structure.xml' into "${project.buildDir}/buildWar/web/war/META-INF/" } } }
对于单一 WAR(singleWAR)配置,这个任务稍有不同:
task buildWar(type: CubaWarBuilding) { webXmlPath = 'modules/web/web/WEB-INF/single-war-web.xml' appProperties = ['cuba.automaticDatabaseUpdate' : true] appHome = '${app.home}' doAfter = { copy { from 'jboss-deployment-structure.xml' into "${project.buildDir}/buildWar/war/META-INF/" } } }
如果项目还包含了 Polymer 模块,在
single-war-web.xml
文件中添加下面这些配置:<servlet> <servlet-name>default</servlet-name> <init-param> <param-name>resolve-against-context-root</param-name> <param-value>true</param-value> </init-param> </servlet>
-
在项目根目录,创建
jboss-deployment-structure.xml
文件,在里面添加 WildFly 部署描述文件:
<?xml version="1.0" encoding="UTF-8"?> <jboss-deployment-structure xmlns="urn:jboss:deployment-structure:1.0"> <deployment> <exclusions> <module name="org.apache.commons.logging" /> <module name="org.apache.log4j" /> <module name="org.jboss.logging" /> <module name="org.jboss.logging.jul-to-slf4j-stub" /> <module name="org.jboss.logmanager" /> <module name="org.jboss.logmanager.log4j" /> <module name="org.slf4j" /> <module name="org.slf4j.impl" /> <module name="org.slf4j.jcl-over-slf4j" /> </exclusions> </deployment> </jboss-deployment-structure>
-
执行
buildWar
任务创建 WAR 包。
-
-
从
build\distributions\war
目录拷贝app-core.war
和app.war
到 WildFly 目录\wildfly\standalone\deployments
。 -
重启 WildFly 服务。
-
可以通过
http://localhost:8080/app
访问应用,日志文件会保存在应用程序主目录的C:\Users\UserName\app_home\logs
目录下。
5.3.4. 部署 WAR 至 Tomcat Windows 服务
-
在 build.gradle 末尾添加 buildWar 任务:
task buildWar(type: CubaWarBuilding) { appHome = './app_home' singleWar = false includeContextXml = true includeJdbcDriver = true appProperties = ['cuba.automaticDatabaseUpdate': true] }
如果目标 Tomcat 服务的参数跟快速部署里用到的本地 Tomcat 的参数不同,需要提供相应的应用程序属性。比如,如果目标 Tomcat 运行在 9999 端口,任务定义会是这样:
task buildWar(type: CubaWarBuilding) { appHome = './app_home' singleWar = false includeContextXml = true includeJdbcDriver = true appProperties = [ 'cuba.automaticDatabaseUpdate': true, 'cuba.webPort': 9999, 'cuba.connectionUrlList': 'http://localhost:9999/app-core' ] }
可以指定另外一个
context.xml
文件用来设置生产环境的数据库,示例:task buildWar(type: CubaWarBuilding) { appHome = './app_home' singleWar = false includeContextXml = true includeJdbcDriver = true appProperties = ['cuba.automaticDatabaseUpdate': true] coreContextXmlPath = 'modules/core/web/META-INF/production-context.xml' }
-
执行
buildWar
Gradle 任务。会在项目build/distributions
目录生成app.war
和app-core.war
文件。gradlew buildWar
-
下载并执行 Tomcat 8 Windows Service Installer。
-
切换到安装好的服务的
bin
目录,使用管理员权限执行tomcat8w.exe
。 在 Java 标签页,设置 Maximum memory pool 为 1024MB。然后在 General 标签页重启服务。 -
在 Java Options 字段添加
-Dfile.encoding=UTF-8
。 -
拷贝项目生成的
app.war
和app-core.war
文件到 Tomcat 服务的webapps
目录。 -
启动 Tomcat 服务。
-
在浏览器打开
http://localhost:8080/app
。
5.3.5. 部署 WAR 至 Tomcat Linux 服务
以下示例在 Ubuntu 16.04 测试通过
-
在 build.gradle 末尾添加 buildWar 任务。可以指定不同的
context.xml
文件来设置生产环境数据库连接:task buildWar(type: CubaWarBuilding) { appHome = '${catalina.base}/work' singleWar = true includeContextXml = true includeJdbcDriver = true appProperties = ['cuba.automaticDatabaseUpdate': true] webXmlPath = 'modules/web/web/WEB-INF/single-war-web.xml' coreContextXmlPath = 'modules/core/web/META-INF/war-context.xml' }
如果目标 Tomcat 服务的参数跟快速部署里用到的本地 Tomcat 的参数不同,需要提供相应的应用程序属性。比如,如果目标 Tomcat 运行在 9999 端口,任务定义会是这样:
task buildWar(type: CubaWarBuilding) { appHome = './app_home' singleWar = false includeContextXml = true includeJdbcDriver = true appProperties = [ 'cuba.automaticDatabaseUpdate': true, 'cuba.webPort': 9999, 'cuba.connectionUrlList': 'http://localhost:9999/app-core' ] }
-
执行
buildWar
Gradle 任务。会在项目build/distributions
目录生成app.war
文件。gradlew buildWar
-
安装 Tomcat 8 Linux Service:
sudo apt-get install tomcat8
-
拷贝项目生成的
app.war
文件到 Tomcat 服务的/var/lib/tomcat8/webapps
目录。Tomcat 服务默认是使用
tomcat8
用户来启动的。所以webapps
目录的所有者也是tomcat8
。 -
创建配置文件
/usr/share/tomcat8/bin/setenv.sh
包含以下设置:export CATALINA_OPTS="$CATALINA_OPTS -Xmx1024m"
-
重启 Tomcat 服务:
sudo service tomcat8 restart
-
在浏览器打开
http://localhost:8080/app
。
5.3.6. UberJAR 部署
这是在生产环境运行 CUBA 应用程序最简单的方法。可以通过 buildUberJar Gradle 任务来构建一个包含所有依赖的 JAR 文件(可以参考 Studio 的 *Deployment > UberJAR setting 界面),然后可以在命令行使用 java
来运行应用程序:
java -jar app.jar
所以应用程序的参数都是在构建时候定义的,但是可以在运行时覆盖这些参数(参阅以下)。web 应用程序默认的端口是 8080
,地址是 http://host:8080/app
。如果项目中有 Polymer UI,默认的 Polymer 客户端地址是 http://host:8080/app-front
。
如果为 Middleware 和 Web 客户款各自构建了单独的 JAR 包,可以用相同的方式运行:
java -jar app-core.jar
java -jar app.jar
web 客户端的默认端口是 8080
,它会尝试连接中间件的默认地址 localhost:8079
。所以如果在两个单独的窗口中运行了上面的两行命令,可以通过 http://localhost:8080/app
地址访问 web 客户端。
可以通过 Java 系统参数的方式提供应用程序属性来更改构建时候设置的应用程序默认参数。而且,端口号、上下文名称和 Jetty 配置文件的路径都可以通过命令行参数修改。
- 命令行参数
-
-
port
- 定义嵌入的 HTTP 服务的端口,示例:java -jar app.jar -port 9090
需要注意的是,如果客户端和 core 模块分别构建了单独的 JAR 包,且为 core 模块指定了端口,那么要为客户端模块提供带有相应 block 地址的 cuba.connectionUrlList 应用程序属性,示例:
java -jar app-core.jar -port 7070 java -Dcuba.connectionUrlList=http://localhost:7070/app-core -jar app.jar
-
contextName
- 应用程序 block 的 web 上下文名称。比如,如果需要在http://localhost:8080/sales
地址访问 web 客户端,运行如下命令:java -jar app.jar -contextName sales
-
frontContextName
- Polymer UI web 上下文名称(对单一 JAR,web JAR 或者 portal JAR 都有效)。 -
portalContextName
- 单一 JAR 里面配置 portal 上下文名称。 -
jettyEnvPath
- Jetty 环境配置文件的路径。用来重写构建时候指定的coreJettyEnvPath
参数的值。这个新值可以设定为绝对路径或者工作目录的相对路径。 -
jettyConfPath
- Jetty 服务配置文件的路径。用来重写构建时候指定的webJettyConfPath/coreJettyConfPath/portalJettyConfPath
参数的值。这个新值可以设定为绝对路径或者工作目录的相对路径。
-
- 应用程序主目录
- 日志
-
如果需要更改内嵌的日志设置,可以通过 Java 系统参数
logback.configurationFile
来提供一个指向新配置文件的 URL,示例:java -Dlogback.configurationFile=file:./logback.xml -jar app.jar
这里假设
logback.xml
文件是在启动服务的目录下。如果需要正确的设定日志输出目录,需要确保
logback.xml
文件里的logDir
属性指定了应用程序主目录的logs
子目录。<configuration debug="false"> <property name="logDir" value="${app.home}/logs"/> <!-- ... -->
- 停止应用程序
-
可以通过下面几种方式平和的停掉应用程序服务:
-
在应用程序运行的终端窗口按下 Ctrl+C
-
在类 Unix 系统执行
kill <PID>
-
发送停止键值(比如,特定字符序列)到应用程序的特定端口,这个端口是在启动应用程序的时候通过命令行指定的。有以下命令行参数:
-
stopPort
- 监听停止键值或者发送停止键值的端口。 -
stopKey
- 停止键值。默认值SHUTDOWN
。 -
stop
- 通过发送停止键值停掉其它进程。
-
示例:
# 启动应用 1 并且在 9090 监听 SHUTDOWN java -jar app.jar -stopPort 9090 # 启动应用 2 并且在 9090 监听 MYKEY java -jar app.jar -stopPort 9090 -stopKey MYKEY # 关停应用 1 java -jar app.jar -stop -stopPort 9090 # 关停应用 2 java -jar app.jar -stop -stopPort 9090 -stopKey MYKEY
-
5.3.6.1. 为 UberJAR 配置 HTTPS
下面的示例是 UberJAR 部署的情况下配置自签发认证的 HTTPS。
-
使用 JDK 自带的
Java Keytool
工具生成密钥和认证:keytool -keystore keystore.jks -alias jetty -genkey -keyalg RSA
-
在项目根目录配置带有 SSL 配置的
jetty.xml
文件:<Configure id="Server" class="org.eclipse.jetty.server.Server"> <Call name="addConnector"> <Arg> <New class="org.eclipse.jetty.server.ServerConnector"> <Arg name="server"> <Ref refid="Server"/> </Arg> <Set name="port">8090</Set> </New> </Arg> </Call> <Call name="addConnector"> <Arg> <New class="org.eclipse.jetty.server.ServerConnector"> <Arg name="server"> <Ref refid="Server"/> </Arg> <Arg> <New class="org.eclipse.jetty.util.ssl.SslContextFactory"> <Set name="keyStorePath">keystore.jks</Set> <Set name="keyStorePassword">password</Set> <Set name="keyManagerPassword">password</Set> <Set name="trustStorePath">keystore.jks</Set> <Set name="trustStorePassword">password</Set> </New> </Arg> <Set name="port">8443</Set> </New> </Arg> </Call> </Configure>
keyStorePassword
,keyManagerPassword
和trustStorePassword
需要按照Keytool
的设置来配置。 -
在构建任务的配置中添加
jetty.xml
:task buildUberJar(type: CubaUberJarBuilding) { singleJar = true coreJettyEnvPath = 'modules/core/web/META-INF/jetty-env.xml' appProperties = ['cuba.automaticDatabaseUpdate' : true] webJettyConfPath = 'jetty.xml' }
-
按照部署 UberJAR 章节的介绍构建 Uber JAR。
-
将
keystore.jks
跟项目的 JAR 放在一个目录下,然后启动 Uber JAR。通过浏览器访问
https://localhost:8443/app
。
5.3.7. 使用 Docker 部署
本章节介绍了如何在 Docker 容器内部署 CUBA 应用程序。
我们将使用在快速开始部分开发的示例项目作为例子,迁移到 PostgreSQL 数据库,并构建 UberJAR,最后运行在容器中。事实上,构建为 WAR 的应用程序也能在容器化的 Tomcat 中运行,但是需要做更多的配置,所以如果只是示例,我们就用 UberJAR。
- 配置并构建 UberJAR
-
从 https://github.com/cuba-platform/sample-sales-cuba7 克隆示例程序,并且在 CUBA Studio中 打开 。
首先,将数据库类型改为 PostgreSQL:
-
在主菜单点击 CUBA > Project Properties 然后切换到 Data Stores 标签页。
-
在 Database type 字段选择 PostgreSQL 然后点击 OK。
-
在主菜单点击 CUBA > Generate Database Scripts。Studio 打开包含自动生成脚本的 Database Scripts 窗口。点击 Save and close。
-
在主菜单点击 CUBA > Create Database。Studio 会在本地 PostgreSQL 服务创建
sales
数据库。
接下来,配置构建 UberJAR 的 Gradle 任务。
-
在主菜单点击 CUBA > Deployment > UberJAR Settings。
-
勾选 Build Uber JAR 和 Single Uber JAR 复选框。
-
点击 Logback configuration file 字段旁边的 Generate 按钮。
-
点击 Custom Jetty environment file 字段旁边的 Generate 按钮。确保选择了 PostgreSQL 然后在 Database URL 字段输入
postgres
替换localhost
。这个配置是在使用下面描述的容器化的数据库时需要的。 -
点击 OK。Studio 会在
build.gradle
文件中添加 构建 UberJar 任务。 -
为了保证日志文件在正确的位置,打开生成的
etc/uber-jar-logback.xml
文件,修改logDir
属性:<property name="logDir" value="${app.home}/logs"/>
还有,确保 Logback 配置文件限制了
org.eclipse.jetty
logger 的级别至少为INFO
。如果文件中没有该 logger,可以添加:<logger name="org.eclipse.jetty" level="INFO"/>
执行创建 JAR 的命令:
./gradlew buildUberJar
-
- 创建 Docker 镜像
-
现在创建
Dockerfile
然后构建我们应用程序的 Docker 镜像。-
在项目中创建
docker-image
目录。 -
从
build/distributions/uberJar
复制 JAR 文件至该目录。 -
使用下面的内容创建一个
Dockerfile
文件:FROM openjdk:8 COPY . /opt/sales CMD java -Dapp.home=/opt/sales-home -jar /opt/sales/app.jar
app.home
Java 系统参数定义了应用程序主目录的文件夹,这里会存放日志文件和应用程序创建的其它文件。当运行容器时,我们能将该目录映射至宿主机的一个目录,这样能方便访问日志以及其它数据,包括上传至 FileStorage 的文件。现在构建镜像:
-
在项目根目录打开终端窗口。
-
运行构建命令,在
-t
选项后面传递镜像名称以及Dockerfile
所在的文件夹:docker build -t sales docker-image
当执行
docker images
命令时,确保能显示sales
镜像。 -
- 运行应用程序和数据库容器
-
现在应用程序已经准备好可以在容器中运行了,但是我们还需要一个容器化的 PostgreSQL 数据库。为了管理两个容器 - 一个应用程序,一个数据库,我们使用 Docker Compose。
在项目根目录创建
docker-compose.yml
文件,使用以下内容:version: '2' services: postgres: image: postgres:9 environment: - POSTGRES_DB=sales - POSTGRES_USER=cuba - POSTGRES_PASSWORD=cuba volumes: - /Users/me/sales-home:/opt/sales-home ports: - "5433:5432" web: image: sales ports: - "8080:8080"
需要注意此文件中这些部分:
-
volumes
部分将容器的/opt/sales-home
目录,也就是应用程序的主目录,映射到宿主机的/Users/me/sales-home
目录。也就是说,应用程序的之日可以通过宿主机的/Users/me/sales-home/logs
目录访问。 -
PostgreSQL 内部端口 5432 映射至宿主机的 5433 端口,避免与宿主机可能运行的 PostgreSQL 实例相冲突。使用该端口,可以在容器外访问数据库,比如,做数据库备份:
pg_dump -Fc -h localhost -p 5433 -d sales -U cuba > /Users/me/sales.backup
-
应用程序容器开放了端口 8080,所以应用程序 UI 可以通过
http://localhost:8080/app
在宿主机访问。
要启动应用程序和数据库,在
docker-compose.yml
文件所在的文件夹打开终端,运行:docker-compose up
-
5.3.8. 部署至 Jelastic Cloud
下面是构建应用程序并部署至 Jelastic 云的示例。
注意,目前只支持使用 PostgreSQL 或者 HSQL 数据库的项目。 |
-
首先,浏览器打开 Jelastic 云创建一个免费的测试账号。
-
创建一个将要部署应用程序 WAR 的新环境:
-
点击 New Environment。
-
在显示的窗口中指定配置的值:兼容性强的环境需要有 Java 8,Tomcat 8 以及 PostgreSQL 9.1+(如果项目使用 PostgreSQL 的话)。在 Environment Name 字段,设置一个唯一的环境名称并点击 Create。
-
如果创建的环境使用 PostgreSQL,你会收到一封带有数据库连接信息的 email。使用 email 里面的链接打开数据库管理界面,然后创建一个空数据库。数据库名称可以稍后在自定义的
context.xml
文件设置。
-
-
使用 CUBA Studio 构建 Single WAR 文件:
-
在主菜单选择 CUBA > Deployment > WAR Settings。
-
勾选 Build WAR 复选框。
-
在 Application home directory 字段输入
..
。 -
勾选 Include JDBC driver 和 Include Tomcat’s context.xml 复选框。
-
如果项目使用了 PostgreSQL,点击 Custom context.xml path 旁边的 Generate 按钮。设置数据库的用户名、密码、主机以及之前创建的数据库名。
-
勾选 Single WAR for Middleware and Web Client 复选框。
-
点击 Custom web.xml path 旁边的 Generate 按钮。Studio 将会生成一个特殊的单一 WAR
web.xml
,组合了中间件和 Web 客户端应用程序 block。 -
在 App properties 字段填写
cuba.logDir
属性:appProperties = ['cuba.automaticDatabaseUpdate': true, 'cuba.logDir': '${catalina.base}/logs']
-
点击 OK 按钮。Studio 会在
build.gradle
文件添加 buildWar 任务。task buildWar(type: CubaWarBuilding) { includeJdbcDriver = true includeContextXml = true appProperties = ['cuba.automaticDatabaseUpdate': true, 'cuba.logDir' : '${catalina.base}/logs'] webXmlPath = 'modules/web/web/WEB-INF/single-war-web.xml' appHome = '..' coreContextXmlPath = 'modules/core/web/META-INF/war-context.xml' }
-
如果项目使用的 HSQLDB,在
build.gradle
中找到buildWar
任务,然后添加hsqlInProcess = true
属性,这样可以在部署 WAR 文件的时候运行嵌入的 HSQL 服务。确保没有设置coreContextXmlPath
属性。task buildWar(type: CubaWarBuilding) { appProperties = ['cuba.automaticDatabaseUpdate': true, 'cuba.logDir': '${catalina.base}/logs'] appHome = '..' includeContextXml = true webXmlPath = 'modules/web/web/WEB-INF/single-war-web.xml' includeJdbcDriver = true hsqlInProcess = true }
-
命令行运行
buildWar
启动构建过程:gradlew buildWar
最后,会在
build\distributions\war
项目子文件夹创建app.war
。
-
-
使用 部署 War Gradle 任务将 WAR 文件部署至 Jelastic。
task deployWar(type: CubaJelasticDeploy, dependsOn: buildWar){ email = **** password = **** hostUrl = 'app.j.layershift.co.uk' environment = 'my-env-1' }
-
部署过程完成后,应用程序已经在 Jelastic 云上可用了。在浏览器输入
<environment>.<hostUrl>
即可打开项目。比如:
http://my-env-1.j.layershift.co.uk
也可以用 Jelastic 环境面板的 Open in Browser 按钮打开应用程序。
5.3.9. 部署至 Bluemix Cloud
CUBA Studio 通过几个简单步骤支持 IBM® Bluemix® 云部署。
Bluemix 云部署目前只支持使用 PostgreSQL 数据库的项目。HSQLDB 只支持 in-process 的情况,也就是说,每次应用程序重启时,会重建数据库,之前的用户数据会丢失。 |
-
创建一个 Bluemix 账号。下载并且安装:
-
Bluemix CLI: http://clis.ng.bluemix.net/ui/home.html
-
Cloud Foundry CLI: https://github.com/cloudfoundry/cli/releases
-
确保
bluemix
和cf
命令在命令行窗口有效。如果不行的话,添加 Bluemix 的 bin 目录,比如\IBM\Bluemix\bin
到PATH
环境变量。
-
-
在 Bluemix 创建一个空间(Space),需要的话,可以在一个空间里面使用几个应用程序。
-
在空间中创建一个应用服务:Create App → CloudFoundry Apps → Tomcat。
-
给应用指定一个名称。名称需要是唯一的,因为这个名称会作为应用程序 URL 的一部分。
-
创建一个数据库服务,点击空间 dashboard 中的 Create service 然后选择 ElephantSQL。
-
打开应用管理并且将数据库服务连接至应用程序。点击 Connect Existing。要使改动生效,系统需要重新加载(restaging/updating)应用程序。目前暂时不需要做这一步,因为应用程序会重新部署。
-
数据库服务连接上之后,数据库的用户密码可以通过点击 View Credentials 看到。数据库的属性存在程序运行时的
VCAP_SERVICES
环境变量里面,可以通过cf env
命令看到。创建的数据库也可以从空间外面访问到,因此可以从开发环境连接线上的数据库。 -
设置 CUBA 项目运行在 PostgreSQL 数据库上(跟 Bluemix 类似的数据库环境)
-
生成数据库脚本然后启动本地 Tomcat 服务。确保应用程序启动没问题。
-
生成 WAR 文件用来部署到 Tomcat。
-
在 CUBA 项目视图的 Project 部分点击 Deployment > WAR Settings 。
-
勾选全部的复选框启用所有的功能,因为正确的部署是 Single WAR 带有 JDBC 驱动和
context.xml
。 -
点击 Custom context.XML field 旁边的 Generate 按钮。在弹出的对话框中填写 Bluemix 里面创建的数据库的用户密码信息。
从 DB 服务拿到的
uri
里面包含数据库的用户密码信息,按照下面这个示例使用:{ "elephantsql": [ { "credentials": { "uri": "postgres://ixbtsvsq:F_KyeQjpEdpQfd4n0KpEFCYyzKAbN1W9@qdjjtnkv.db.elephantsql.com:5432/ixbtsvsq", "max_conns": "5" } } ] }
Database user:
ixbtsvsq
Database password:
F_KyeQjpEdpQfd4n0KpEFCYyzKAbN1W9
Database URL:
qdjjtnkv.db.elephantsql.com:5432
Database name:
ixbtsvsq
-
点击 Generate 按钮生成单一 WAR 需要的自定义
web.xml
文件。 -
保存设置。使用 Studio 的
buildWar
Gradle 任务或者命令行生成 WAR 包。成功的话,会在项目的
build/distributions/war/
目录生成app.war
。
-
-
在项目的根目录手动创建
manifest.yml
文件。文件内容需要包含下列信息:applications: - path: build/distributions/war/app.war memory: 1G instances: 1 domain: eu-gb.mybluemix.net name: myluckycuba host: myluckycuba disk_quota: 1024M buildpack: java_buildpack env: JBP_CONFIG_TOMCAT: '{tomcat: { version: 8.0.+ }}' JBP_CONFIG_OPEN_JDK_JRE: '{jre: { version: 1.8.0_+ }}'
这里的参数:
-
path
- WAR 包文件的相对路径。 -
memory
- 默认的内存限制是 1G。可以根据应用的具体情况增加或者减少,也可以通过 Bluemix 的 web 页面调整。需要注意内存大小直接影响运行费用。 -
name
- 上面在云服务里创建的 Tomcat 应用的名称(取决于项目地址,参考App URL
,比如https://myluckycuba.eu-gb.mybluemix.net/
)。 -
host
- 跟名称一样。 -
env
- 设置 Tomcat 版本和 Java 版本的环境变量。
-
-
在命令行切换到 CUBA 项目的根目录。
cd your_project_directory
-
连接到 Bluemix(再次检查域名)
cf api https://api.eu-gb.bluemix.net
-
登录 Bluemix 账号。
cf login -u your_bluemix_id -o your_bluemix_ORG
-
部署 WAR 到 Tomcat
cf push
push
命令从manifest.yml
文件中读取所有需要的参数信息。 -
可以通过 Bluemix 的 web 页面 dashboard 的 Log 标签页查看 Tomcat 服务的日志,也可以在命令行通过以下命令查看:
cf logs cuba-app --recent
-
部署过程完成后,可以在浏览器通过
host.domain
URL 来访问。这个 URL 会显示在 Cloud Foundry Apps 表格的 ROUTE 字段。
5.3.10. 部署至 Heroku Cloud
本章节介绍如何部署 CUBA 应用程序至 Heroku® 云平台。
这个指导基于部署使用 PostgreSQL 数据库的项目做介绍。 |
5.3.10.1. 部署 WAR 至 Heroku
- Heroku 账号
-
首先,使用浏览器在 Heroku 创建一个账号,免费账号类型
hobby-dev
已经足够使用。然后登录账号,并且点击界面顶部的 New 按钮创建新项目。选择一个唯一的名称(或者这个字段空着等着自动分配)并且选择一个服务器地址。然后 Heroku 会创建一个应用,比如
morning-beach-4895
。第一次使用的时候,Heroku 会切换到 Deploy 标签页,选择使用 Heroku 的 Git 部署方式。
- Heroku CLI
-
-
在电脑安装 Heroku CLI。
-
切换到 CUBA 项目目录。接下来这个目录会用
$PROJECT_FOLDER
来代称。 -
在
$PROJECT_FOLDER
目录打开命令行窗口并输入:heroku login
-
按提示输入用户名密码。下面的步骤应该不需要再给这个项目输入用户名密码了。
-
安装 Heroku CLI 插件:
heroku plugins:install heroku-cli-deploy
-
- PostgreSQL 数据库
-
使用浏览器访问 Heroku 数据 网页
可以选择已有的 Postgres 数据库或者创建一个新的。以下步骤描述怎么创建一个新数据库。
-
找到 Heroku Postgres 区域点击 Create one
-
然后新界面点击 Install Heroku Postgr…
-
下拉列表选择 Heroku 应用程序并连接数据库
-
选择账号付费计划(比如
hobby-dev
)
或者也可以通过 Heroku CLI 来安装 PostgreSQL:
heroku addons:create heroku-postgresql:hobby-dev --app morning-beach-4895
这里的
morning-beach-4895
是 Heroku 应用的名称。然后可以在 Resources 标签页看到新建的数据库。数据库已经连接 Heroku 应用程序。通过这个方式可以看到数据库的用户名密码:切换到 Heroku 数据库的 Datasource 界面,下翻到 Administration 区域然后点击 View credentials 按钮。
Host compute.amazonaws.com Database d2tk User nmmd Port 5432 Password 9c05 URI postgres://nmmd:9c05@compute.amazonaws.com:5432/d2tk
-
- 项目部署设置
-
-
假设 CUBA 项目使用的是 PostgreSQL 数据库。
-
在 Studio 打开 CUBA 项目,在 CUBA 项目视图的 Deployment 部分双击 WAR settings 打开 WAR settings 窗口,按照下面的介绍对选项进行配置。
-
勾选 Build WAR
-
设置 application home 目录为
.
(当前目录) -
勾选 Include JDBC driver
-
勾选 Include Tomcat’s context.xml
-
点击 Custom context.xml path 旁边的 Generate 按钮,在弹窗中填写数据库连接信息
-
点开生成的
modules/core/web/META-INF/war-context.xml
文件,检查连接参数和用户名密码: -
勾选 Single WAR for Middleware and Web Client
-
点击 Custom web.xml path 旁边的 Generate 按钮。
-
拷贝下面的代码然后粘贴到 App properties 字段:
[ 'cuba.automaticDatabaseUpdate' : true ]
-
保存部署设置。
-
-
- 构建 WAR 文件
-
执行
buildWar
Gradle 任务构建 WAR 文件:gradlew buildWar
通过 Studio 的 Build 菜单命令可以预先创建 Gradle wrapper 以便在命令行窗口使用
gradlew
命令。 - 应用程序配置
-
-
从 https://mvnrepository.com/artifact/com.github.jsimone/webapp-runner 下载 Tomcat Webapp Runner。Webapp Runner 的版本需要跟 Tomcat 的版本匹配,比如,Webapp Runner 8.5.11.3 版本能兼容 Tomcat 8.5.11 版本。下载下来的 JAR 包重命名成
webapp-runner.jar
然后放到$PROJECT_FOLDER
目录。 -
从 https://mvnrepository.com/artifact/org.apache.tomcat/tomcat-dbcp 下载 Tomcat DBCP。版本需要能兼容 Tomcat 的版本,比如 8.5.11。创建
$PROJECT_FOLDER/libs
目录,将下载的 JAR 文件重命名为tomcat-dbcp.jar
然后放到这个目录。 -
在
$PROJECT_FOLDER
目录创建新文件Procfile
。文件包含如下内容:web: java $JAVA_OPTS -cp webapp-runner.jar:libs/* webapp.runner.launch.Main --enable-naming --port $PORT build/distributions/war/app.war
-
- Git 配置
-
在
$PROJECT_FOLDER
目录打开终端窗口,运行以下命令:git init heroku git:remote -a morning-beach-4895 git add . git commit -am "Initial commit"
- 应用部署
-
打开终端窗口,运行以下命令:
在 *nix 系统:
heroku jar:deploy webapp-runner.jar --includes libs/tomcat-dbcp.jar:build/distributions/war/app.war --app morning-beach-4895
Windows 系统:
heroku jar:deploy webapp-runner.jar --includes libs\tomcat-dbcp.jar;build\distributions\war\app.war --app morning-beach-4895
在 Heroku 的 dashboard 打开 Resources 标签页。能看到一个新的 Dyno(Heroku 的服务实例),还有从
Procfile
读取出来的命令:应用程序现在正在部署了,可以查看日志了解部署进度。
- 日志监控
-
在命令行窗口等待弹出
https://morning-beach-4895.herokuapp.com/ deployed to Heroku
消息。需要跟踪查看应用系统的日志,可以使用以下命令:
heroku logs --tail --app morning-beach-4895
部署成功之后,可以通过浏览器访问类似 https://morning-beach-4895.herokuapp.com
这样的 URL 来访问服务。
或者可以通过 Heroku dashboard 的 Open app 按钮打开应用。
5.3.10.2. 从 GitHub 部署到 Heroku
这个向导是给使用 GitHub 开发 CUBA 项目的开发者作为参考。
- Heroku 账号
-
首先,使用浏览器在 Heroku 创建一个账号,免费账号类型
hobby-dev
已经足够使用。然后登录账号,并且点击界面顶部的 New 按钮创建新项目。选择一个唯一的名称(或者这个字段空着等着自动分配)并且选择一个服务器地址。然后 Heroku 会创建一个应用,比如
space-sheep-02453
,这个就是 Heroku 应用程序名称。第一次使用的时候,Heroku 会切换到 Deploy 标签页,选择使用 GitHub 部署方式。根据界面提示完成 GitHub 账号的授权。 点击 Search 按钮列出所有的 Git 仓库(repositories),连接需要使用的仓库。当 Heroku 应用程序连接到 GitHub 之后,就可以启用 Automatic Deploys - 自动部署。自动部署可以在每次触发 Git push 事件的时候实现自动重新部署 Heroku 应用。在这个向导里面,启用了此自动部署功能。
- Heroku CLI
-
-
安装 Heroku CLI
-
在电脑的任何目录打开命令行窗口,并输入:
heroku login
-
按提示输入用户名密码。下面的步骤应该不需要再给这个项目输入用户名密码了。
-
- PostgreSQL 数据库
-
-
返回浏览器打开 Heroku dashboard
-
切换到 Resources 标签页
-
点击 Find more add-ons 按钮查找数据库插件
-
找到 Heroku Postgres 区域然后点击。根据界面介绍选择 Login to install / Install Heroku Postgres
或者也可以通过 Heroku CLI 来安装 PostgreSQL:
heroku addons:create heroku-postgresql:hobby-dev --app space-sheep-02453
这里的
space-sheep-02453
是 Heroku 应用的名称。然后可以在 Resources 标签页看到新建的数据库。数据库已经连接 Heroku 应用程序。通过这个方式可以看到数据库的用户名密码:切换到 Heroku 数据库的 Datasource 界面,下翻到 Administration 区域然后点击 View credentials 按钮。
Host compute.amazonaws.com Database zodt User artd Port 5432 Password 367f URI postgres://artd:367f@compute.amazonaws.com:5432/zodt
-
- 项目部署设置
-
-
切换到项目根目录 (
$PROJECT_FOLDER
) -
拷贝
modules/core/web/META-INF/context.xml
到modules/core/web/META-INF/heroku-context.xml
-
修改
heroku-context.xml
文件将数据库连接信息更新进去(参考下面的例子):<Context> <Resource driverClassName="org.postgresql.Driver" maxIdle="2" maxTotal="20" maxWaitMillis="5000" name="jdbc/CubaDS" password="367f" type="javax.sql.DataSource" url="jdbc:postgresql://compute.amazonaws.com/zodt" username="artd"/> <Manager pathname=""/> </Context>
-
- 构建配置
-
将下面的 Gradle 任务添加到
$PROJECT_FOLDER/build.gradle
task stage(dependsOn: ['setupTomcat', ':app-core:deploy', ':app-web:deploy']) { doLast { // replace context.xml with heroku-context.xml def src = new File('modules/core/web/META-INF/heroku-context.xml') def dst = new File('deploy/tomcat/webapps/app-core/META-INF/context.xml') dst.delete() dst << src.text // change port from 8080 to heroku $PORT def file = new File('deploy/tomcat/conf/server.xml') file.text = file.text.replace('8080', '${port.http}') // add local.app.properties for core application def coreConfDir = new File('deploy/tomcat/conf/app-core/') coreConfDir.mkdirs() def coreProperties = new File(coreConfDir, 'local.app.properties') coreProperties.text = ''' cuba.automaticDatabaseUpdate = true ''' // rename deploy/tomcat/webapps/app to deploy/tomcat/webapps/ROOT def rootFolder = new File('deploy/tomcat/webapps/ROOT') if (rootFolder.exists()) { rootFolder.deleteDir() } def webAppDir = new File('deploy/tomcat/webapps/app') webAppDir.renameTo( new File(rootFolder.path) ) // add local.app.properties for web application def webConfDir = new File('deploy/tomcat/conf/ROOT/') webConfDir.mkdirs() def webProperties = new File(webConfDir, 'local.app.properties') webProperties.text = ''' cuba.webContextName = / ''' } }
- Procfile
-
在 Heroku 启动应用的命令是通过一个特殊文件
Procfile
来传递的。在$PROJECT_FOLDER
创建Procfile
文件,使用以下内容:web: cd ./deploy/tomcat/bin && export 'JAVA_OPTS=-Dport.http=$PORT' && ./catalina.sh run
此处提供了 Tomcat 在启动 Catalina 脚本时需要的 JAVA_OPTS 环境变量值。
- Premium 插件
-
如果项目使用了 CUBA Premium 插件,需要为 Heroku 应用程序设置额外的变量。
-
打开 Heroku dashboard。
-
切换到 Settings 标签页。
-
展开 Config Variables 区域,点击 Reveal Config Vars 按钮。
-
添加新 Config Vars,使用授权码的前后两部分(短横分隔)分别作为 用户名 和 密码:
CUBA_PREMIUIM_USER | username CUBA_PREMIUM_PASSWORD | password
-
- Gradle wrapper
-
项目需要使用 Gradle wrapper。可以通过 CUBA Studio 来添加:Build > Create or update Gradle wrapper。
-
在
$PROJECT_FOLDER
创建system.properties
文件,使用如下内容(示例是假设本地安装的 JDK 是 1.8.0_121 版本):java.runtime.version=1.8.0_121
-
检查确保这些文件不在
.gitignore
里:Procfile
,system.properties
,gradlew
,gradlew.bat
和gradle
-
将这些文件添加到仓库并且提交
git add gradlew gradlew.bat gradle/* system.properties Procfile git commit -am "Added Gradle wrapper and Procfile"
-
- 应用程序部署
-
一旦提交并且推送了所有的改动到 GitHub,Heroku 会自动开始重新部署应用。
git push
构建的过程可以通过 Heroku dashboard 的 Activity 标签页看到,点击 View build log 监控构建日志。
构建过程完成后,可以通过浏览器访问类似
https://space-sheep-02453.herokuapp.com
这样的 URL 来访问服务。可以通过 Heroku dashboard 的 Open app 按钮打开应用。 - 日志监控
-
需要跟踪查看应用系统的日志,可以使用以下命令:
heroku logs --tail --app space-sheep-02453
Tomcat 日志也可以通过应用系统的 Menu > Administration > Server Log 查看。
5.3.10.3. 部署容器至 Heroku
按照使用 Docker 部署章节介绍的内容配置单一 UberJAR。创建 Heroku 账号然后安装 Heroku CLI,可以参考部署 WAR 至 Heroku 章节。
用以下命令创建应用程序并且连接数据库
heroku create cuba-sales-docker --addons heroku-postgresql:hobby-dev
等这个任务完成之后需要在 jetty-env.xml
文件中配置 Heroku 创建的数据库连接的用户名和密码。
-
浏览器打开 https://dashboard.heroku.com 。
-
选择创建的项目,打开 Resources 标签页,选择数据库。
-
在新打开的窗口中,打开 Settings 标签页并且点击 View Credentials 按钮。
切换到 IDE 打开 jetty-env.xml
文件。修改 URL(host 和数据库名称),用户名和密码。从网页拷贝用户名和密码到这个文件。
<?xml version="1.0"?>
<!DOCTYPE Configure PUBLIC "-" "http://www.eclipse.org/jetty/configure_9_0.dtd">
<Configure id='wac' class="org.eclipse.jetty.webapp.WebAppContext">
<New id="CubaDS" class="org.eclipse.jetty.plus.jndi.Resource">
<Arg/>
<Arg>jdbc/CubaDS</Arg>
<Arg>
<New class="org.apache.commons.dbcp2.BasicDataSource">
<Set name="driverClassName">org.postgresql.Driver</Set>
<Set name="url">jdbc:postgresql://<Host>/<Database></Set>
<Set name="username"><User></Set>
<Set name="password"><Password></Set>
<Set name="maxIdle">2</Set>
<Set name="maxTotal">20</Set>
<Set name="maxWaitMillis">5000</Set>
</New>
</Arg>
</New>
</Configure>
执行以下 Gradle 任务创建单一 Uber JAR:
gradle buldUberJar
另外,需要对 Dockerfile
进行一些修改。首先,如果使用的是 Heroku 的免费账号,需要限制应用程序使用的内存大小;然后需要从 Heroku 获得应用程序的端口号并添加到镜像中。
修改后的 Dockerfile
示例:
### Dockerfile
FROM openjdk:8
COPY . /usr/src/cuba-sales
CMD java -Xmx512m -Dapp.home=/usr/src/cuba-sales/home -jar /usr/src/cuba-sales/app.jar -port $PORT
通过下面的命令设置 Git:
git init
heroku git:remote -a cuba-sales-docker
git add .
git commit -am "Initial commit"
登录容器仓库,是 Heroku 存储镜像的地址:
heroku container:login
接下来,构建镜像并推送到容器仓库:
heroku container:push web
这里 web
是应用程序的处理类型(process type)。当执行这个命令的时候,Heroku 默认会使用当前目录的 Dockerfile
来构建镜像,然后把镜像推送到 Heroku。
当部署流程完成后,可以通过浏览器打开类似这样的 URL https://cuba-sales-docker.herokuapp.com/app 访问应用。
或者可以通过 Heroku dashboard 的 Open app 按钮打开应用。
打开运行中应用的第三种方式是使用如下命令(链接最后需要添加 app
, https://cuba-sales-docker.herokuapp.com/app ):
heroku open
5.4. Tomcat 的代理设置
对于系统集成的情况,可能需要一个代理服务器。本章节介绍配置 Nginx HTTP-server 作为 CUBA 应用程序的代理服务。
设置代理的时候,别忘了设置 cuba.webAppUrl 的值。 |
- NGINX
-
对于 Nginx,下面有两种配置方法,所有示例都在 Ubuntu 16.04 测试通过。
假设,web 应用程序运行在 http://localhost:8080/app
。
Tomcat 也需要增加一点配置。 |
- Tomcat 配置
-
首先,在 Tomcat 配置文件
conf/server.xml
中添加Valve
属性,拷贝粘贴以下代码:<Valve className="org.apache.catalina.valves.RemoteIpValve" remoteIpHeader="X-Forwarded-For" requestAttributesEnabled="true" internalProxies="127\.0\.0\.1" />
然后重启 Tomcat 服务:
sudo service tomcat8 restart
这个配置可以使得不需要修改 web 应用程序的情况下使用 Tomcat 来分发 Nginx headers。
然后安装 Nginx:
sudo apt-get install nginx
浏览器打开
http://localhost
确保 Nginx 能工作,应该打开的是 Nginx 的欢迎页面。现在可以删除默认 Nginx 网页的符号链接(symlink)了:
rm /etc/nginx/sites-enabled/default
下一步,按照下面两种方式的任意一种配置代理。
- 直接代理
-
直接代理的情况下,网页请求都是由代理处理,然后直接透明的转发给应用程序。
创建 Nginx 网站配置文件
/etc/nginx/sites-enabled/direct_proxy
:server { listen 80; server_name localhost; location /app/ { proxy_set_header Host $host; proxy_set_header X-Forwarded-Server $host; proxy_set_header X-Forwarded-Proto $scheme; # Required to send real client IP to application server proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Real-IP $remote_addr; # Optional timeouts proxy_read_timeout 3600; proxy_connect_timeout 240; proxy_http_version 1.1; # Required for WebSocket: proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_pass http://127.0.0.1:8080/app/; } }
重启 Nginx:
sudo service nginx restart
现在可以通过
http://localhost/app
访问应用程序。
- 转发路径
-
这个例子说明如何将应用程序的 URL 路径从 /app 更换成 /,就像应用程序是直接部署在根目录(类似部署在/ROOT 的效果)。这种方法允许通过
http://localhost
访问应用程序。创建 Nginx 网站配置文件
/etc/nginx/sites-enabled/root_proxy
:server { listen 80; server_name localhost; location / { proxy_set_header Host $host; proxy_set_header X-Forwarded-Server $host; proxy_set_header X-Forwarded-Proto $scheme; # Required to send real client IP to application server proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Real-IP $remote_addr; # Optional timeouts proxy_read_timeout 3600; proxy_connect_timeout 240; proxy_http_version 1.1; # Required for WebSocket: proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_pass http://127.0.0.1:8080/app/; # Required for folder redirect proxy_cookie_path /app /; proxy_set_header Cookie $http_cookie; proxy_redirect http://localhost/app/ http://localhost/; } }
然后重启 Nginx
sudo service nginx restart
现在可以通过
http://localhost
访问应用程序。
类似的部署指令对于 Jetty, |
5.5. UberJAR 的代理服务配置
本章节介绍配置 Nginx HTTP-server 作为 CUBA Uber JAR 应用程序的代理。
- NGINX
对于 Nginx,下面有两种配置方法,所有示例都在 Ubuntu 16.04 测试通过。
-
Direct Proxy - 直接代理
-
Redirect to Path - 转发路径
假设,web 应用程序运行在 http://localhost:8080/app
。
Uber JAR 应用程序使用 Jetty 9.2 web 服务器。需要提前在 JAR 中配置 Jetty 用来分发 Nginx headers。 |
- Jetty 配置
-
-
使用内部的 jetty.xml
首先,在项目根目录创建 Jetty 配置文件
jetty.xml
,拷贝以下代码:<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_0.dtd"> <Configure id="Server" class="org.eclipse.jetty.server.Server"> <New id="httpConfig" class="org.eclipse.jetty.server.HttpConfiguration"> <Set name="outputBufferSize">32768</Set> <Set name="requestHeaderSize">8192</Set> <Set name="responseHeaderSize">8192</Set> <Call name="addCustomizer"> <Arg> <New class="org.eclipse.jetty.server.ForwardedRequestCustomizer"/> </Arg> </Call> </New> <Call name="addConnector"> <Arg> <New class="org.eclipse.jetty.server.ServerConnector"> <Arg name="server"> <Ref refid="Server"/> </Arg> <Arg name="factories"> <Array type="org.eclipse.jetty.server.ConnectionFactory"> <Item> <New class="org.eclipse.jetty.server.HttpConnectionFactory"> <Arg name="config"> <Ref refid="httpConfig"/> </Arg> </New> </Item> </Array> </Arg> <Set name="port">8080</Set> </New> </Arg> </Call> </Configure>
在
build.gradle
中添加webJettyConfPath
属性到buildUberJar
任务:task buildUberJar(type: CubaUberJarBuilding) { singleJar = true coreJettyEnvPath = 'modules/core/web/META-INF/jetty-env.xml' appProperties = ['cuba.automaticDatabaseUpdate' : true] webJettyConfPath = 'jetty.xml' }
可以在 Studio 中通过 Deployment > UberJAR Settings 来生成
jetty-env.xml
文件,或者用以下代码:<?xml version="1.0"?> <!DOCTYPE Configure PUBLIC "-" "http://www.eclipse.org/jetty/configure_9_0.dtd"> <Configure id='wac' class="org.eclipse.jetty.webapp.WebAppContext"> <New id="CubaDS" class="org.eclipse.jetty.plus.jndi.Resource"> <Arg/> <Arg>jdbc/CubaDS</Arg> <Arg> <New class="org.apache.commons.dbcp2.BasicDataSource"> <Set name="driverClassName">org.postgresql.Driver</Set> <Set name="url">jdbc:postgresql://<Host>/<Database></Set> <Set name="username"><User></Set> <Set name="password"><Password></Set> <Set name="maxIdle">2</Set> <Set name="maxTotal">20</Set> <Set name="maxWaitMillis">5000</Set> </New> </Arg> </New> </Configure>
使用以下命令构建 Uber JAR:
gradlew buildUberJar
应用程序 JAR 包会被放置在
build/distributions/uberJar
目录,名称为app.jar
。运行应用程序:
java -jar app.jar
按照 Tomcat 部分的介绍安装和配置 Nginx。
按照选择配置 Nginx 的方法不同,可以通过
http://localhost/app
或者http://localhost
地址访问应用。 -
使用外部的 jetty.xml
按照上面的描述使用和项目根目录的
jetty.xml
文件相同的配置文件。将这个文件放在其它目录,比如说用户主目录,不需要修改build.gradle
里的buildUberJar
任务。使用下列命令构建 Uber JAR:
gradlew buildUberJar
应用程序打包完了放在
build/distributions/uberJar
目录,默认名称是app.jar
。首先,用带参数
-jettyConfPath
运行程序:java -jar app.jar -jettyConfPath jetty.xml
然后按照 Tomcat 部分的介绍安装和配置 Nginx。
按照选择配置 Nginx 的方法和
jetty.xml
文件的配置不同,可以通过http://localhost/app
或者http://localhost
地址访问应用。
-
5.6. 应用程序扩展
本章节介绍在负载增加或者有更强的容错需求的时候怎样对包含 Middleware 和 Web Client blocks 的 CUBA 应用程序进行扩展。
扩展级别 1. 两个 blocks 部署在同一个应用服务内 这是使用标准快速部署流程的最简单情况。 在这种情况下,Web Client 和 Middleware 之间的数据传输性能可以达到最大化,因为当启用 cuba.useLocalServiceInvocation 应用程序属性时,可以跳过网络堆栈直接调用 Middleware 服务。 |
|
扩展级别 2. Middleware 和 web 客户端部署在不同的应用程序服务内 这个选择可以在两个应用服务器之间分散负载,从而更好的使用两个服务器的资源。还有,用这种部署方式从 web 用户来的负载会对其它进程的执行影响小一些。这里的其它进程是指处理其它客户端类型、运行计划任务还有潜在可能的一些从中间层(middle layer)来的集成任务(integration tasks)。 对服务器资源的要求:
在这种部署选择或者更复杂的部署情况下,web 客户端的 cuba.useLocalServiceInvocation 应用程序属性应该设置成 |
|
扩展级别 3. Web 客户端集群搭配单一 Middleware 服务 这种部署选择是用在,由于并发用户数太大导致 Web 客户端的内存需求超过了单一 JVM 的承载能力的场景。集群中多个 Web 客户端启动后,用户连接通过负载均衡来处理。所有的 Web 客户端都连接同一个 Middleware 服务器。 多个 Web 客户端自动提供了一定级别的容错。但是,不支持在客户端之间复制 HTTP 会话,如果有一个 web 客户端服务器计划外宕机了,所有连到这个服务器的用户需要重新登录应用程序。 这个部署选择的配置在 配置 Web 客户端集群 介绍。 |
|
扩展级别 4. Web 客户端集群搭配 Middleware 集群 这个方案是最强大的部署选择,能提供全面的 Web 客户端和 Middleware 容错性和负载均衡。 连接到 Web 客户端的用户通过负载均衡接入。多个 Web 客户端服务器跟 Middleware 集群协作提供服务。Middleware 服务不需要额外的负载均衡 - 通过 cuba.connectionUrlList 应用程序属性已经足够定义 Middleware 服务器 URL 列表。另外还可通过集成 Apache ZooKeeper 插件用来做 Middleware 服务的动态发现。 多个 Middleware 服务会交换用户会话、锁等信息。这样的话,Middleware 可以提供全容错 - 其中一台服务宕机之后,另一台可用的 Middleware 服务会接过用户的请求继续处理,从而不会影响终端用户的感受。 这个部署选择的配置在 配置 Middleware 集群 介绍。 |
5.6.1. 配置 Web 客户端集群
本章节介绍如下部署配置:
host1
和 host2
服务器上部署了实现 Web 客户端 block 的 app
Tomcat 实例。用户通过 http://host0/app
地址访问负载均衡,负载均衡会转发用户请求到不同的服务。host3
服务器部署了实现 Middleware block 的 app-core
Tomcat 实例。
5.6.1.1. 安装和配置负载均衡
这里介绍在 Ubuntu 14.04 上安装 Apache HTTP Server 作为负载均衡。
-
安装 Apache HTTP Server 和 mod_jk 模块:
$ sudo apt-get install apache2 libapache2-mod-jk
-
用以下内容替换
/etc/libapache2-mod-jk/workers.properties
文件内容:workers.tomcat_home= workers.java_home= ps=/ worker.list=tomcat1,tomcat2,loadbalancer,jkstatus worker.tomcat1.port=8009 worker.tomcat1.host=host1 worker.tomcat1.type=ajp13 worker.tomcat1.connection_pool_timeout=600 worker.tomcat1.lbfactor=1 worker.tomcat2.port=8009 worker.tomcat2.host=host2 worker.tomcat2.type=ajp13 worker.tomcat2.connection_pool_timeout=600 worker.tomcat2.lbfactor=1 worker.loadbalancer.type=lb worker.loadbalancer.balance_workers=tomcat1,tomcat2 worker.jkstatus.type=status
-
添加下面的这些内容到
/etc/apache2/sites-available/000-default.conf
:<VirtualHost *:80> ... <Location /jkmanager> JkMount jkstatus Order deny,allow Allow from all </Location> JkMount /jkmanager/* jkstatus JkMount /app loadbalancer JkMount /app/* loadbalancer </VirtualHost>
-
重启 Apache HTTP 服务:
$ sudo service apache2 restart
5.6.1.2. 设置多个 Web 客户端服务器
在下面的示例中,配置文件的路径都是按照使用快速部署的情况提供的。 |
在 Tomcat 1 和 Tomcat 2 服务器,做以下配置:
-
在
tomcat/conf/server.xml
文件中,添加jvmRoute
参数,其值为在负载均衡配置中为tomcat1
和tomcat2
设置的 worker 的名称:<Server port="8005" shutdown="SHUTDOWN"> ... <Service name="Catalina"> ... <Engine name="Catalina" defaultHost="localhost" jvmRoute="tomcat1"> ... </Engine> </Service> </Server>
-
在
tomcat/conf/app/local.app.properties
中设置下列应用程序属性:cuba.useLocalServiceInvocation = false cuba.connectionUrlList = http://host3:8080/app-core cuba.webHostName = host1 cuba.webPort = 8080 cuba.webContextName = app
cuba.webHostName, cuba.webPort 和 cuba.webContextName 参数对于 Web 客户端集群来说不是必须的,但是这些参数为在平台的其它功能中辨识服务器提供了方便,比如 JMX 控制台。另外 User Sessions 界面的 Client Info 属性会显示用户目前使用的 Web 客户端的标识符。
5.6.2. 配置 Middleware 集群
本章节介绍如下部署配置:
host1
和 host2
服务器上部署了实现 Web 客户端 block 的 app
Tomcat 实例。这些服务的集群配置在前一章节介绍过了。host3
和 host4
服务器部署了实现 Middleware block 的 app-core
Tomcat 实例。这两个服务器配置了交互和共享用户会话、锁等等信息。
在下面的示例中,配置文件的路径都是按照使用快速部署的情况提供的。 |
5.6.2.1. 配置连接 Middleware 集群
为了使客户端 blocks 能跟多个 Middleware 服务器工作,服务器 URL 列表需要通过 cuba.connectionUrl 应用程序属性来配置。在 Web 客户端,这个配置可以写在 tomcat/conf/app/local.app.properties
:
cuba.useLocalServiceInvocation = false
cuba.connectionUrlList = http://host3:8080/app-core,http://host4:8080/app-core
cuba.webHostName = host1
cuba.webPort = 8080
cuba.webContextName = app
第一次用户会话远程连接的中间件服务器是随机选择的,而且对于整个会话的生命周期这个服务器都是固定的("sticky session")。从匿名用户会话来的请求或者不带用户会话的请求不固定在特定的服务器,也是随机选择服务器执行。
选择中间件服务器的算法是由 cuba_ServerSorter
bean 提供的,这个 bean 默认是由 RandomServerSorter
类实现。也可以在项目中提供自定义的实现。
5.6.2.2. 配置多个 Middleware 服务交互
多个 Middleware 服务器可以共同维护用户会话共享列表和其它类对象,还能协调处理过期的缓存。每个中间件服务都需要开启 cuba.cluster.enabled 参数启用这个功能。以下是 tomcat/conf/app-core/local.app.properties
文件的示例:
cuba.cluster.enabled = true
cuba.webHostName = host3
cuba.webPort = 8080
cuba.webContextName = app-core
对于中间件服务来说,需要设定正确的 cuba.webHostName,cuba.webPort 和 cuba.webContextName 这三个属性的值,这样能用这三个属性组成唯一的服务器 ID。
服务之间的交互机制是基于 JGroups。平台为 JGroups 提供了两个配置文件:
-
jgroups.xml
- 基于 UDP 的协议栈,适用于启用了广播通信的本地网络。这个配置当集群功能开启的时候会被默认使用。 -
jgroups_tcp.xml
- 基于 TCP 的协议栈,适用于任何网络。使用这个协议要求在TCP.bind_addr
和TCPPING.initial_hosts
参数中显式设定集群成员的地址。如果需要使用这个配置,需要设定 cuba.cluster.jgroupsConfig 这个应用程序属性。
为了配置环境中的 JGroups 参数,从 cuba-core-<version>.jar
的根目录拷贝合适的 jgroups.xml
文件到项目的 core 模块或者 tomcat/conf/app-core
目录,并且修改这个文件。
ClusterManagerAPI
bean 提供 Middleware 集群中服务器交互的编程接口。可以在应用程序中使用,需要时可参考 JavaDocs 和平台代码的用法。
5.6.2.3. 使用 ZooKeeper 来协调集群
为了能让中间件服务之间互相通信,并且帮助客户端请求中间件服务,有个应用程序组件可以启用动态发现中间件服务。这个组件是基于集成 Apache ZooKeeper 完成的,ZooKeeper 是个中心化的服务,用来维护配置信息。当项目引入这个组件之后,运行应用程序 block 的时候只需要指定一个 ZooKeeper 的静态地址。Middleware 服务将会通过在 ZooKeeper 目录发布它们的地址的方式进行广播,然后发现机制会向 ZooKeeper 请求能用的服务器的地址。如果一个中间件服务宕机了,这个服务会被马上从目录自动移除或者等到超时再被移除。
5.6.3. 服务器 ID
Server ID 用来在 Middleware 集群中提供服务器的可靠标识。标识符的格式是 host:port/context
:
tezis.haulmont.com:80/app-core
192.168.44.55:8080/app-core
标识符是使用配置参数 cuba.webHostName,cuba.webPort,cuba.webContextName 来组合的,所以对于在集群中的 Middleware blocks 来说,设定这几个参数非常重要。
Server ID 可以通过 ServerInfoAPI
bean 来获取,或者通过 ServerInfoMBean 这个 JMX 接口获取。
5.7. 使用 JMX 工具
本章节介绍在基于 CUBA 的应用程序中使用 Java Management Extensions 的各方面内容。
5.7.1. 内置 JMX 控制台
cuba 应用程序组件的 Web 客户端模块包含 JMX 对象查看和编辑工具。工具的入口是注册在 jmxConsole
标识符下的 com/haulmont/cuba/web/app/ui/jmxcontrol/browse/display-mbeans.xml
界面,可以通过标准应用程序菜单的 Administration > JMX Console 访问。
不需要额外的配置,这个控制台能显示当前用户正在运行的 web 客户端 JVM 内注册的所有 JMX 对象。因此,在最简单的情况下,当所有的应用程序 block 都部署到一个 web 容器实例的时候,JMX 控制台可以访问所有层(tier)的 JMX beans 甚至包括 JVM 和 web 容器的 JMX 对象。
应用程序 beans 的名称都带有一个拥有这些 bean 的 web-app 名称的前缀。比如,app-core.cuba:type=CachingFacade
bean 会被 app-core web-app 加载,该 web-app 实现了中间件 block;而 app.cuba:type=CachingFacade
bean 会被 app web-app 加载,该 web-app 实现了 Web 客户端 block。
JMX 控制台也可以访问远程 JVM 的 JMX 对象。这个功能在应用程序 blocks 部署在几台不同的 web 容器是很有用,比如,分开部署的 web 客户端和中间件。
需要连接远程 JVM,可以通过控制台的 JMX Connection 字段选择一个之前建立的连接或者创建一个新连接:
要建立连接,需要提供 JMX 主机地址,端口,登录名和密码。还有个 Node name - 节点名称 字段,如果在指定的地址监测到 CUBA 应用服务的 block 的话,会自动填充。这种情况下,这个字段的值会被定义成此 block 的cuba.webHostName 和 cuba.webPort 的组合,这样也利于辨认包含这个服务的服务器。如果连接是通过第三方 JMX 接口建立的,那么 Node name 字段会有"Unknown JMX interface"值。但是也可以手动修改它。
为了提供远程 JVM 连接,JVM 需要做适当的配置(见下面)。
5.7.2. 设置远程 JMX 连接
本章节介绍需要进行远程 JMX 工具连接的 Tomcat 启动配置。
5.7.2.1. Windows 下 Tomcat JMX 配置
-
按照下面方法编辑
bin/setenv.bat
:set CATALINA_OPTS=%CATALINA_OPTS% ^ -Dcom.sun.management.jmxremote ^ -Djava.rmi.server.hostname=192.168.10.10 ^ -Dcom.sun.management.jmxremote.ssl=false ^ -Dcom.sun.management.jmxremote.port=7777 ^ -Dcom.sun.management.jmxremote.authenticate=true ^ -Dcom.sun.management.jmxremote.password.file=../conf/jmxremote.password ^ -Dcom.sun.management.jmxremote.access.file=../conf/jmxremote.access
这里,
java.rmi.server.hostname
参数需要包含服务运行的机器的实际 IP 地址或者 DNS 名称;com.sun.management.jmxremote.port
用来设置 JMX 工具连接的端口号。 -
编辑
conf/jmxremote.access
文件,需要包含连接 JMX 的用户名以及他们的访问级别,示例:admin readwrite
-
编辑
conf/jmxremote.password
文件,需要包含 JMX 用户的密码,示例:admin admin
-
对于运行 Tomcat 服务的用户,他们应当只有密码文件的只读权限。可以通过以下方式配置权限:
-
打开命令行窗口,切换到 conf 目录
-
执行命令:
cacls jmxremote.password /P "domain_name\user_name":R
这里
domain_name\user_name
是用户所在的域和用户名称。 -
这个命令执行之后,这个文件在 Explorer 会显示锁住状态(有个锁的图标)。
-
-
如果 Tomcat 是按照 Windows 服务的方式安装的,那么服务需要以具有能访问 jmxremote.password 权限的用户身份启动。需要注意的是,这种情况下会忽略
bin/setenv.bat
文件,相应的 JVM 启动参数应该通过配置 Tomcat 服务的应用程序来设置。
5.7.2.2. Linux 下 Tomcat JMX 配置
-
按照下面方法编辑
bin/setenv.sh
:CATALINA_OPTS="$CATALINA_OPTS -Dcom.sun.management.jmxremote \ -Djava.rmi.server.hostname=192.168.10.10 \ -Dcom.sun.management.jmxremote.port=7777 \ -Dcom.sun.management.jmxremote.ssl=false \ -Dcom.sun.management.jmxremote.authenticate=true" CATALINA_OPTS="$CATALINA_OPTS -Dcom.sun.management.jmxremote.password.file=../conf/jmxremote.password -Dcom.sun.management.jmxremote.access.file=../conf/jmxremote.access"
这里,
java.rmi.server.hostname
参数需要包含服务运行的机器的实际 IP 地址或者 DNS 名称;com.sun.management.jmxremote.port
用来设置 JMX 工具连接的端口号。 -
编辑
conf/jmxremote.access
文件,需要包含连接 JMX 的用户名以及他们的访问级别,示例:admin readwrite
-
编辑
conf/jmxremote.password
文件,需要包含 JMX 用户的密码,示例:admin admin
-
对于运行 Tomcat 服务的用户,他们应当只有密码文件的只读权限。可以通过以下方式配置权限:
-
打开命令行窗口,切换到 conf 目录
-
执行命令:
chmod go-rwx jmxremote.password
-
5.8. 服务推送设置
CUBA 应用程序的后台任务机制采用服务推送技术。可能需要对应用程序或者代理服务做一些额外的配置。
默认情况下,服务推送使用的 WebSocket 协议。下面这些应用程序属性影响平台的服务推送功能:
下面这些信息是从 Vaadin 网页上摘录的 - 为你的环境配置推送。
- Chrome 错误消息 ERR_INCOMPLETE_CHUNKED_ENCODING
-
这个完全正常,表示长轮询(long-polling)推送(push)连接由于第三方软件的原因断掉了。典型的场景就是当浏览器和服务器之间有个代理,如果这个代理配置了时限规则,一旦超时就断掉连接。浏览器应当在这个事件发生之后重新建立跟服务器的连接。
- Tomcat 8 + Websockets 错误消息
-
java.lang.ClassNotFoundException: org.eclipse.jetty.websocket.WebSocketFactory$Acceptor
这个错误暗示在 classpath 里面可能配置有 Jetty。这样的话运行环境就可能被误导,会尝试使用 Jetty 的 WebSocket 而不是使用 Tomcat 的。一个常见的原因是因为不小心部署了
vaadin-client-compiler
,这里面有个 Jetty 依赖(比如 SuperDevMode 需要 Jetty)。
- Glassfish 4 + Streaming
-
Glassfish 4 要求 comet 选项启动,这样才能使用 streaming。
设置
(Configurations → server-config → Network Config → Protocols → http-listener-1 → HTTP → Comet Support)
或者使用
asadmin set server-config.network-config.protocols.protocol.http-listener-1.http.comet-support-enabled="true"
- Glassfish 4 + Websockets
-
如果使用的 Glassfish 4.0,升级到 Glassfish 4.1 就没问题了。
- Weblogic 12 + Websockets
-
使用 WebLogic 12.1.3 或者更高的版本。WebLogic 12 默认指定了 WebSocket 超时的时间是 30 秒。为了避免定期重连,可以设置 WebLogic 的初始参数
weblogic.websocket.tyrus.session-max-idle-timeout
为-1
(无时限)或者一个比较大的值(单位是毫秒)。
- JBoss EAP 6.4 + Websockets
-
JBoss EAP 6.4 支持 websockets,但是默认这个功能是禁用的。要启用 WebSocket,需要更改 JBoss 使用 NIO 连接器:
$ bin/jboss-cli.sh --connect
然后运行下面这个命令:
batch /subsystem=web/connector=http/:write-attribute(name=protocol,value=org.apache.coyote.http11.Http11NioProtocol) run-batch :reload
然后把下面这些内容添加到 WEB-INF/jboss-web.xml 文件,这个文件加到 war 包里面启用 WebSockets:
<jboss-web version="7.2" xmlns="http://www.jboss.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.jboss.com/xml/ns/javaee schema/jboss-web_7_2.xsd"> <enable-websockets>true</enable-websockets> </jboss-web>
- 重复资源错误
-
如果服务的日志包含
Duplicate resource xyz-abc-def-ghi-jkl. Could be caused by a dead connection not detected by your server. Replacing the old one with the fresh one
这意味着,首先,浏览器连接到了服务端,并且使用了提供的推送连接的标识符。一切工作正常。然后,浏览器(很可能跟之前的是同一个)使用了相同的标识符再次连接,但是从服务端来看,之前的浏览器连接还是有效的。于是服务端就把之前的连接断掉然后在日志打印了这个警告。
这个情况发生一般来说主要是在浏览器跟服务端之间有个代理,代理配置了在一定的无活动超时之后就断掉打开的连接(在服务端执行 push 命令之前不会有数据发送)。依据 TCP/IP 的工作原理,服务端根本不知道连接已经断了,然后认为旧连接还能用。
有两种选择避免这个问题:
-
如果能控制中间的代理,配置代理不要限时或者不要断掉推送连接(连接以
/PUSH
结尾) -
如果知道代理的时限是多少,配置应用程序的推送连接时限稍微小于代理的这个时限值,从而使服务端能在代理断掉连接之前先主动结束空闲连接并且知晓这个状态。
-
设置
cuba.web.pushLongPolling
参数为true
来启用长轮询传输替代 WebSocket。 -
设置
cuba.web.pushLongPollingSuspendTimeoutMs
参数来控制 push 连接的时限,单位毫秒。
-
即便没有配置代理,服务端也就能知道连接断掉的状态,但是还是有一些可能导致丢失推送的数据。如果碰巧服务端在连接刚刚好断掉之后推送了数据,服务端不会意识到这些数据推送到了一个关闭的连接中(根据 socket 的工作原理,特别是 Java 中 socket 的工作原理)。所以通过禁用连接时限或者设置服务端连接时限小于浏览器端也能解决这个潜在问题。
-
- 使用代理
-
如果用户使用了一个不支持 WebSocket 的代理连接应用程序服务,设置
cuba.web.pushLongPolling
为true
并且增加代理请求的超时时限至 10 分钟或者更多。如下是一个 Nginx 使用 WebSocket 的 web 服务的设置:
location / { proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Server $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_read_timeout 3600; proxy_connect_timeout 240; proxy_set_header Host $host; proxy_set_header X-RealIP $remote_addr; proxy_pass http://127.0.0.1:8080/; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; }
5.9. 应用程序健康检查 URL
每个作为 web 应用程序部署的应用程序模块都提供了健康检查的 URL。对这个 URL 进行 HTTP GET 操作,如果返回是 ok
的话,表明这个模块可以开始运行了。
不同 block 的 URL 路径列表如下:
-
Middleware:
/remoting/health
-
Web Client:
/rest/health
-
Web Portal:
/rest/health
所以对于名称叫 app
并且部署在 localhost:8080
的应用程序,这些 URL 将会是:
-
http://localhost:8080/app-core/remoting/health
-
http://localhost:8080/app/rest/health
-
http://localhost:8080/app-portal/rest/health
可以使用 cuba.healthCheckResponse 应用程序属性将返回的 ok
替换成任意字符串。
监控检查的控制器也会发送类型为 HealthCheckEvent
的事件。因此可以添加自定义的检查应用健康的逻辑。 GitHub 的这个例子 演示了 web 层的一个 bean 监听健康检查的事件,并且调用中间件服务,最后在数据库做了一次操作。
6. 安全子系统
CUBA 框架使用下面的方法来控制访问权限:
6.1. WEB 安全
CUBA 开发的应用程序安全吗?
CUBA 框架作为开发框架遵循了良好的安全实践,为 web 应用程序中最通常最易受攻击的部分做了自动防护。平台架构提供了安全编程模型,可以使得开发者专注于业务和程序逻辑。
-
用户界面(UI)状态和验证
-
Cross-Site Scripting (XSS) - 跨站脚本攻击
Web 客户端集成了对于跨站脚本攻击的防护措施。它会在数据在用户浏览器做渲染之前,先把数据转成 HTML 实体。
-
Cross-Site Request Forgery (CSRF) 跨站请求伪造
所有在客户端和服务端之间的请求都包含了用户会话特定的 CSRF token。所有服务端跟客户端的通信都由 Vaadin 框架 来处理,所以不需要手动处理去包含 CSRF token。
-
Web 服务
所有 Web 客户端的通信都通过一个支持 RPC 请求的 web 服务来做。项目中不会开放包含业务逻辑代码的 web 服务,因此减少了应用中的攻击入口点。
通用 REST API 接口也为登录用户或者匿名用户自动提供了角色、权限和其它的安全限制。
-
SQL 注入
平台使用基于 EclipseLink 的 ORM 层,EclipseLink 已经做了针对 SQL 注入的防护。SQL 查询的参数是通过参数数组的形式传递给 JDBC 的,而不是跟查询语句做字符串拼接。
6.2. 安全子系统组件
CUBA 安全子系统主要组件展示在下图。
下面是这些组件的概览:
Security management screens - 安全管理界面 – 系统管理员可用的一组界面,用来配置用户访问权限。
Login screen - 登录界面 − 系统登录窗口。通过用户名和密码提供用户认证。数据库保存密码哈希值以保证安全。
登录时会创建 UserSession 对象。这是当前认证用户主要的安全元素,包含了用户数据访问权限的信息。
用户登录过程的描述,请参考 登录。
Roles - 角色 − 用户角色。角色是一个系统对象,一方面用来匹配进行特殊功能操作的权限,另一方面用来限定一组用户必须拥有这些权限。
有以下类型的权限:
-
Screen Permissions - 界面权限 − 打开界面的权限。
-
Entity Operation Permissions - 实体操作权限 − 对实体进行 CRUD 操作的权限。
-
Entity Attribute Permissions - 实体属性权限 − 访问任意实体属性的权限,包括:修改,只读,禁止访问。也可参考 实体属性访问控制。
-
Specific Permissions - 特殊权限 − 对一些定义了名称的功能的访问权限控制。
-
UI Permissions - UI 组件权限 − 控制对于界面元素的访问。
6.2.1. 登录界面
登录界面提供使用登录名和密码登录的功能。登录名大小写敏感。
Web Client 中的 Remember Me - 记住我 复选框可以通过应用程序属性cuba.web.rememberMeEnabled 设置。标准登录页中可选语言的下拉列表可以通过应用程序属性 cuba.localeSelectVisible 和 cuba.availableLocales 设置。
其它相关的应用程序属性有: cuba.web.loginDialogDefaultUser,cuba.web.loginDialogDefaultPassword 和 cuba.web.loginDialogPoweredByLinkVisible。
在 Web Client,可以在 Studio 中自定义或完全替换标准登录窗口。在项目树选择 Generic UI 并点击右键菜单的 New > Screen。然后在 Legacy Screen Templates 标签页选择 Login Window 模板。会在 Web 模块创建新的 ext-loginWindow.xml
文件,并自动注册到 web-screens.xml
中。参阅 Web 客户端基础设施。
平台提供防暴力破解密码机制:参阅应用程序属性 cuba.bruteForceProtection.enabled。
6.2.2. 用户
译者注 — 这里很多英文是界面上内容的描述,所以在翻译时,界面元素保留了很多英文以及对应的中文翻译。
每个系统用户都会有对应的 sec$User
实例,包含唯一登录名,密码 hash 值,访问组引用和角色列表,还有其它属性。用户管理功能在 Administration > Users screen 界面:
除了标准的 create - 创建,update - 更新,和 delete - 删除 动作,还支持以下动作:
-
Copy – 复制,基于当前所选用户快速创建新用户。新用户与所选用户拥有相同的访问组和角色集合。在用户编辑界面可以进一步修改它们。
-
Copy settings – 复制设置。复制用户界面设置到其它一或多个用户。这些设置包括表格展示,分隔面板分隔符位置,过滤器和搜索文件夹。
-
Change password – 为所选用户修改密码。
-
Reset passwords – 为所选用户做以下操作:
-
在 Reset passwords for selected users 重置密码对话框,如果没有选择 Generate new passwords - 生成新密码标记,会给所选用户设置上 Change password at next logon 下次登录修改密码的标记。有这类标记的用户下次登录成功后,会被要求修改密码。
用户想要修改密码的话,需要有
sec$User.changePassword
界面的权限。在配置角色的时候需要注意这一点,尤其是给用户分配 Denying- 拒绝 权限的时候。角色编辑界面中,sec$User.changePassword
界面的设置在 Other screens 其它界面节点内。 -
如果选择了 Generate new passwords flag 生成新密码标记,系统会随机生成密码显示给系统管理员。这些密码可以导出到 XLS 文件然后发送给相关的用户。并且,Change password at next logon 下次登录修改密码的标记也会被设置,保证用户下次登录时修改密码。
-
如果设置了 Generate new passwords 的同时也设置了 Send emails with generated passwords 将密码发送到邮件标记,自动生成的一次性密码会被直接发送给对应的用户,系统管理员不会看到。邮件发送是异步的,所以需要一个特定的计划任务,参考 发送方法 章节。
密码重置邮件的模板可以修改。可以在 core 模块创建本地化模板, reset-password-subject.gsp 和 reset-password-body.gsp 可以做为例子。需要本地化模板时,创建 locale(语种)结尾的文件, platform 可以作为参考。
模板是基于 Groovy
SimpleTemplateEngine
语法,所以支持 Groovy blocks,例如:Hello <% print (user.active ? user.login : 'user') %> <% if (user.email.endsWith('@company.com')) { %> The password for your account has been reset. Please login with the following temporary password: ${password} and immediately create a new one for the further work. Thank you <% } else {%> Please contact your system administrator for a new password. <%}%>
这些模板的数据绑定包括
user
,password
和persistence
变量。也可以使用任何 middleware 中 的 Spring beans(前提是 importAppBeans
,能在AppBeans.get()
方法中使用)。需要覆盖模板的话,在
core
模块的app.properties
中指定以下属性:cuba.security.resetPasswordTemplateBody = <relative path to your file> cuba.security.resetPasswordTemplateSubject = <relative path to your file>
为了在生产线上更容易的配置模板,可以通过配置目录配置或重定义位置, 将属性配置到
local.app.properties
文件。
-
用户编辑界面描述如下:
-
Login – 必输项,唯一登录名。
-
Group – 访问组 。
-
Last name, First name, Middle name – 用户全名的各部分。
-
Name – 基于上述 name 自动生成的用户的全名。生成的规则定义在应用程序属性 cuba.user.fullNamePattern 中,全名也可以手动修改设置。
-
Position – 职位。
-
Language – 语言,用户选择的界面语言。如果要禁止用户选择语言,使用应用程序属性 cuba.localeSelectVisible。
-
Time Zone – 时区 ,显示、输入时期时使用的时区。
-
Email – 电子邮件。
-
Active – 如果没有设置,用户则不能登录系统。
-
Permitted IP Mask – IP 地址掩码,定义用户可以从哪些 IP 地址登录。
掩码是逗号分隔的 IP 地址列表。IPv4 和 IPv6 地址都支持。IPv4 地址应该包含四部分数字用“.”分隔。IPv6 分 8 部分,每部分是 4 个 16 进制码,以冒号分隔。可以使用 "*" 符号来匹配任何值。同一次只支持一种 IP 地址类型(IPv4 或 IPv6)。
例子:
192.168.*.*
-
Roles – 用户角色列表。
-
Substituted Users – 可被替代的用户列表。
6.2.2.1. 用户替代
当某个用户有可替代用户时,应用右上角会显示一个下拉列表,而不是纯文本:
当在这个列表中选择另一用户时,所有已经打开的界面会关闭,替代激活。UserSession.getUser()
方法返回当前登录用户,但是 UserSession.getSubstitutedUser()
会返回被替代的用户。如果没有替代发生,UserSession.getSubstitutedUser()
返回 null
。
在用户编辑界面,通过 Substituted Users - 被替代用户 表格管理可被替代的用户。替代界面描述如下:
-
User – 被编辑用户,该用户会替代其它用户。
-
Substituted user – 可以被替代的用户。
-
Start date, End date – 非必须属性,替代生效时间。该时间区域以外则不能替代。如果不知道时间区域,会一直生效,直到管理删除它。
6.2.2.2. 时区
默认情况下,所以时间有关的值都可以在服务器时区中显示。通过在应用程序 block 中调用 TimeZone.getDefault()
获取时区。默认时区基于操作系统而来,也可以通过设置 Java 系统属性 user.timezone
显式设置。例如,Unix 环境下,给运行于 Tomcat 中的 web client 和 middleware 设置时区为 GMT 时,添加以下代码到 tomcat/bin/setenv.sh
文件:
CATALINA_OPTS="$CATALINA_OPTS -Duser.timezone=GMT"
用户可以查看/编辑与服务器不同时区的时间戳值,有两种方法管理用户时区:
-
管理员可以在用户编辑界面修改。
-
用户自己在 Help > Settings 窗口自行修改。
两种方法中,时区都包含两个域:
-
时区名称,用户可以在下拉列表选择。
-
Auto 复选框,勾选后时区会从当前环境自动获取(对于 web client 是浏览器)。
如果两个域都为空,则不会对该用户做任何时区转换。否则,用户登录时,系统会在 UserSession 中保存时区设置,并在显示或输入时间值时使用它。应用程序代码也可以在需要的时候使用 UserSession.getTimeZone
获取时区的值。
如果时区值在当前会话被使用到,其简称和 GMT 值会在应用程序主窗口的用户名旁边显示。
时区转换会在使用 DateTimeDatatype 类型时发生,比如,时间值。使用( |
6.2.3. 权限许可
permission -权限许可 定义用户对系统对象或功能的权限,例如界面,实体操作等。可以定义允许的权限,也可以定义 prohibition - 拒绝 的权限。
默认情况下,用户对对象拥有允许的权限,除非显式的定义了拒绝权限。 |
权限许可通过 sec$Permission
实体实例控制,包含以下属性:
-
type
– 许可类型,许可施加的对象类型。 -
target
– 许可对象,许可施加的对象。该属性的格式基于许可类型。 -
value
– 许可值,可选值基于许可类型。
许可类型描述如下:
-
PermissionType.SCREEN
– 界面许可界面标识在
target
属性中指定,value
属性值可以为 0(拒绝) 或 1(允许)。界面许可检查发生在组建系统主菜单,和每次调用 Frame 接口的
openWindow()
,openEditor()
,openLookup()
方法时。在应用程序中检查界面许可时,可以使用 Security 接口的
isScreenPermitted()
方法。 -
PermissionType.ENTITY_OP
– 实体操作许可在
target
属性中指定 实体名 + “:” + 操作类型,操作类型有:create
,read
,update
,delete
。例如:library$Book:delete
。value
属性值可以为 0(拒绝)或 1(允许)。实体操作许可检查发生在使用 DataManager,数据感知可视化组件,和标准行为操作数据时,也适用于实体列表。所以,操作权限也会影响 client blocks 和 REST API。但是在 Middleware 通过 EntityManager 操作数据时不会触发实体操作许可检查。
在应用程序中检查实体操作许可时,可以使用 Security 接口的
isEntityOpPermitted()
方法。 -
PermissionType.ENTITY_ATTR
– 实体属性许可在
target
属性中指定 实体名 + “:” + 属性名,例如:library$Book:name
。value
属性值可以为 0(不可见), 1(只读) 或 2(读写)。在应用程序中检查实体属性许可时,可以使用 Security 接口的
isEntityAttrPermitted()
方法。 -
PermissionType.SPECIFIC
– 特定功能许可。特定的许可可以用来取代角色对项目功能的许可/禁止,因为角色是许可的合集。在
target
属性中指定功能标识,value
属性值可以为 0(拒绝)或 1(允许)。项目中特点的许可在配置文件 permissions.xml 中设置。
例如:
@Inject private Security security; private void calculateBalance() { if (!security.isSpecificPermitted("myapp.calculateBalance")) return; ... }
-
PermissionType.UI
– 界面组件许可在
target
属性中指定 界面标识 + “:" + 组件路径。组件路径格式描述详见下一章节。
6.2.4. 角色
译者注 — 这里很多英文是界面上内容的描述,所以在翻译时,界面元素保留了很多英文以及对应的中文翻译。
角色包含权限许可集合,可以给用户分配角色。
一个用户可以有多个角色,角色之间设计为组合(逻辑或)的关系。例如,一个用户拥有角色 A,B 和 C,角色 A 拒绝 X,角色 B 允许 X,角色 C 未明确设置对 X 的权限,那么,X 对用户是允许的。
如果没有角色显式定义某个对象的权限许可,那么用户拥有该对象的许可权限。所以,当没有任何角色显式定义所有对象的权限许可时,或者至少有一个角色定义了许可权限,那么用户拥有所有对象的许可权限。
如果一个用户只有一个角色,并且该角色没有显示设置任何许可权限,或者用户没有任何角色,那么用户拥有所有对象的许可权限。 |
角色列表在 Administration > Roles screen 界面显示。除了标准的 create(创建), update(编辑), 和 delete(删除) 操作,该界面还有一个 Assign to users - 分配给用户 按钮,可以将所选的角色分配给多个用户。
角色编辑界面描述如下。角色属性显示在上方:
-
Name – 角色的唯一名称或标识,必填项。角色创建好以后该名称不能被修改。
-
Localized name – 本地化角色名称。
-
Description – 对于该角色的文本描述。
-
Type – 角色类型,可以是以下值:
-
Standard – 标准,该角色类型提供明确的权限许可。
-
Super – 超级,自动提供所有许可。应该分配给系统管理员,其它角色的拒绝设置都会被它移除。
-
Read-only – 只读,自动拒绝以下实体操作: CREATE-创建,UPDATE-修改,DELETE-删除。因此,带这个角色的用户只能读数据而不能更改(除非用户还拥有其它显式的设置许可上述操作的角色)。
-
Denying – 拒绝,自动拒绝所有对象的访问,除了实体属性。如果需要查看或者更新系统数据,需要额外分配给用户其它显式的允许必要操作的角色。
所有类型的角色都可以再额外的显式的设置权限许可。例如,可以给 Read-only 角色添加修改实体的权限。不过,给 Super 角色添加拒绝访问的设置也没有意义,因为这个角色类型会移除任何的拒绝设置。
-
-
Default role – 默认角色标记。开启了这个标记的角色会自动分配给新创建的用户。
下面是权限许可管理各个标签页的描述。
-
Screens 界面标签页配置界面的权限:
该标签页左侧的树形列表对应应用程序的主菜单。最后一个树节点是 Other screens - 其它界面,包含不在主菜单的界面(例如: 实体编辑界面)。
-
Entities 实体标签页配置实体的操作权限:
Assigned only - 只显示已分配 复选框默认生效,所以列表里只显示对该角色明确设置了的实体。因此,新创建的角色这里显示空白。需要设置权限的时候,取消勾选 Assigned only,然后点击 Apply - 应用。也可以在 Entity - 实体文本框 输入部分实体名称,点击 Apply 来做筛选。
System level - 系统 复选框允许查看选择以
@SystemLevel
注解的系统实体,这类实体默认不会显示。当违反这类限制时,用户会收到错误通知。如果要本地化这类错误信息,需要在主语言包中重写 RowLevelSecurityExceptionHandler。
-
Attributes 属性标签页配置实体属性权限:
Permissions 权限列展示了显式设置过权限的熟悉。具有 modify 修改权限的标记为绿色,具有 read-only 只读权限的标记为蓝色,具有 hide 隐藏权限的标记为红色。
实体列表的展示、筛选类似于 Entities 实体标签页。
如果需要动态改变属性权限,依赖于实体当前状态或者关联实体,使用
SetupAttributeAccessHandler
接口的实体属性访问控制机制。 -
Specific 特定功能标签页配置特定功能权限:
项目配置文件 permissions.xml 配置能分配特定权限的对象名称。
-
UI 界面组件标签页配置界面中组件的权限:
这里允许设置任何界面中组件的权限,包括非数据感知组件(比如容器组件)。配置这类权限需要知道组件 ID,所以需要看源代码来查找组件 ID。
创建这类权限时,在 Screen 界面下拉列表选择需要的界面,在 Component 组件 输入框输入组件 ID,点击 Add - 添加。然后在 Permissions - 权限 面板设置需要的访问权限。
组件 ID 名称的规则是:
-
如果组件属于当前界面,只要输入组件
id
即可。 -
如果组件属于嵌入当前界面的 frame(子框架窗口),需要输入: frame 的 Id + “.” + 组件 Id。
-
如果为 TabSheet 或者 FieldGroup 中的组件设置权限,需要输入:组件 Id + “[” + TabSheet Id 或 FieldGroup Id + “]”。
-
如果为 action 配置权限,需要输入:action 操作的组件 Id + “<” + actionId + “>”, 例如
customersTable<changeGrade>
。
-
6.2.5. 访问组
一个用户只能加入一个访问组,但是用户加入的访问组树形层级以上的约束列表和会话属性都会被继承。
通过 Administration > Access Groups 界面管理访问组:
6.2.5.1. 约束
Constraints - 约束 限制用户访问实体实例。与应用到 类 级别的许可不同,约束是应用到 特定实体实例 的,当实体实例不满足约束条件时,则不允许访问。约束可以设置到增删改查级别。同时,还可以配置自定义约束。
配置到用户访问组的约束,及其树形层级以上的访问组的约束都会对该用户生效。所以,当用户所在访问组树形层级越低,给用户配置的约束会越多。 |
所有客户端层通过标准 DataManager 发起的操作都会触发约束检查. 如果实体不满足约束,添加、更改或删除的时候会抛出 RowLevelSecurityException
异常。
约束检查有三种类型:数据库约束检查,内存约束检查,数据库加内存约束检查。
-
数据库约束检查条件通过 JPQL 子句设置。设置以后会被追加到查询语句之后,这样不满足条件的结果在数据库级别会被过滤掉。数据库检查的约束只能用到查询操作中。
-
内存约束检查通过 Groovy 表达式设置。这类表达式执行在对象图中的实例上,当不满足条件时,数据会被从对象图中过滤掉。
-
数据库加内存约束检查兼有上述二者。
需要创建约束时,打开 Access Groups - 访问组 界面,选择一个 group,在 Constraints - 约束 标签页中点击 Create - 创建:
从 Entity Name - 实体名称 下拉列表中选择一个实体, 从 Operation Type - 操作类型 下拉列表中选择操作类型,基于选择的约束检查类型,用 Join Clause 和 Where Clause 设置 JPQL 条件或者用 Groovy 脚本 字段设置 Groovy 条件。也可以使用 Constraint Wizard - 约束向导,可以直观的创建 JPQL 和 Groovy 条件。当选择自定义操作类型时,会出现 Code 字段,设置特定的 code 来定义约束。
JPQL 编辑器中的 Join Clause 和 Where Clause 字段支持自动填写实体名和对应的属性。按下 Ctrl+Space 可以触发自动填写。在“.”之后触发会列出匹配上下文的属性名,其它情况下,会列出实体列表。 |
JPQL 约束需要遵从以下规则:
-
{E}
需要被做为查询实体的别名,当执行查询语句时,它会被查询语句中真正使用的别名替代。 -
以下预定义常量可以用作 JPQL 参数:
-
Where Clause 字段中的内容会被添加到
where
子句并用and
连接。不需要显式添加where
单词,系统会自动添加。 -
Join Clause 字段中的内容会被添加到
from
子句,该字段需要用逗号“,”开头,使用join
或left join
。
一个简单的约束示例如上图所示:用户只能看到 ref$Car
中 VIN 以 ‘00’ 开头实体数据。
另外一个常见的例子:假如有一个实体与 User
实体为多对多关系,想让用户只能看到跟自己相关的数据,可以在 Where Clause 中使用 JPQL 的 member of
操作符:
(select u from sec$User u where u.id = :session$userId) member of {E}.users
内存约束检查时,UserSession
类型的 userSession
会被传入 Groovy 脚本,可以用它获取当前会话的属性,例如:
{E}.createdBy == userSession.user.login
开发者可以使用以下 Security
接口检查某一实体的约束条件:
-
isPermitted(Entity, ConstraintOperationType)
- 根据操作类型检查是否约束。 -
isPermitted(Entity, String)
- 根据输入字符串检查是否约束。
也可以基于 ItemTrackingAction
连接 action 和特定约束。在 action
的 XML 节点中设置 constraintOperationType
属性或者使用 setConstraintOperationType()
方法设置。
示例:
<table>
...
<actions>
<action id="create"/>
<action id="edit" constraintOperationType="update"/>
<action id="remove" constraintOperationType="delete"/>
</actions>
</table>
违反约束时,会弹出通知给用户。每个约束的提示标题与内容可以在运行时重写,在 Access Groups - 访问组 界面的 Constraints - 约束 标签页,点击 Localization - 本地化,然后就可以设置通知的标题和内容文本了。 |
6.2.5.2. 会话属性
登录时,用户访问组的所有属性集合,以及组以上树形层级的属性集合都会被置于用户会话中。如果不同树形层级有相同的属性,最上层的会生效。所以,在低树形层级组中覆盖属性值是不可能的。如果发现有覆盖倾向,会在服务器日志中显示 WARN
级别日志。
在 Access Groups - 访问组 界面的 Session Attributes - 会话属性 标签页点击 Create - 创建 创建会话属性:
需要指定:唯一的属性名,数据类型和属性值。
在应用程序代码中使用 session 属性:
@Inject
private UserSessionSource userSessionSource;
...
Integer accessLevel = userSessionSource.getUserSession().getAttribute("accessLevel");
使用 session$
前缀,会话属性可以在约束中做为 JPQL 参数使用:
{E}.accessLevel = :session$accessLevel
6.3. 访问权限控制示例
本章节提供一些关于如何配置用户数据访问权限的实践建议。
6.3.1. 配置角色
-
创建一个
Default - 默认
角色,这个角色禁止所有系统权限。最简单的方法是创建一个 Denying - 拒绝 类型的角色。然后为其勾选 Default role 默认复选框,这样所有新用户都默认带有该角色。 -
为各类用户类别创建一系列允许特定权限的角色,有两种策略:
-
粗放化(Coarse-grained)角色 – 每个角色中都配置上某类用户责任范畴所需的所有权限,例如
Sales Manager
销售经理,Accountant
会计。这样的话,除了Default
默认角色,每个用户只分配一个角色。 -
精细化(Fine-grained)角色 – 每个角色都配置为可以操作某类功能。例如
Task Creator
任务创建,References Editor
引用编辑。这样的话,每个用户基于他的责任范畴会分配到多个角色。
两种策略也可以组合使用。为不同的用户类别创建一系列角色来分配权限。
-
-
系统管理员可能会不需要分配任何角色,他们拥有所有系统对象的所有权限。也可以为其分配一个 Super 类型的角色,覆盖所有其它角色指定的限制。
管理功能访问权限
需要给 Denying - 拒绝 角色提供访问管理功能的时候,需要放开一些权限,以下是快速参考。比如,只开放实体日志功能,在相应部分设置提到的权限。
推荐至少提供 sys$FileDescriptor
实体的只读权限,因为这个实体在平台很多地方都会用到:邮件、附件、日志等。
下面提到的权限可以通过相应标签页的 Role
编辑窗口进行配置:Entity - 实体, Screen - 界面 或者 Specific - 特殊功能。
另外,可以通过 cuba.defaultPermissionValuesConfig 应用程序属性配置系统实体的默认访问权限。
- Users - 用户
-
User 实体可以在数据模型中用来做关联引用实体。需要在查询组件或者下拉框组件使用用户实体,只需要设置
sec$User
实体的权限就足够。如果需要使用 Denying - 拒绝 角色创建或者编辑
User
实体,还需要设置以下权限:-
实体:
sec$User
,sec$Group
; (可选)sec$Role
,sec$UserRole
,sec$UserSubstitution
.
读取
sec$UserSubstitution
实体的权限对代替用户功能是至关重要的。-
界面: Users 菜单项,
sec$User.edit
,sec$Group.lookup
; (可选)sec$Group.edit
,sec$Role.edit
,sec$Role.lookup
,sec$User.changePassword
,sec$User.copySettings
,sec$User.newPasswords
,sec$User.resetPasswords
,sec$UserSubstitution.edit
.
-
- Access Groups - 访问组
-
创建或者管理用户访问组以及安全限制。
-
实体:
sec$Group
,sec$Constraint
,sec$SessionAttribute
,sec$LocalizedConstraintMessage
. -
界面: Access Groups 菜单项,
sec$Group.lookup
,sec$Group.edit
,sec$Constraint.edit
,sec$SessionAttribute.edit
,sec$LocalizedConstraintMessage.edit
.
-
- Dynamic Attributes - 动态属性
-
访问额外的实体非持久化属性。
-
实体:
sys$Category
,sys$CategoryAttribute
, 以及数据模型需要的其它实体。 -
界面: Dynamic Attributes 菜单项,
sys$Category.edit
,sys$CategoryAttribute.edit
,dynamicAttributesConditionEditor
,dynamicAttributesConditionFrame
.
-
- User Sessions - 用户会话
-
查看用户会话数据。
-
实体:
sec$User
,sec$UserSessionEntity
. -
界面: User Sessions 菜单项,
sessionMessageWindow
.
-
- Locks - 锁
-
设置实体的悲观锁。
-
实体:
sys$LockInfo
,sys$LockDescriptor
, 以及数据模型需要的其它实体。 -
界面: Locks 菜单项,
sys$LockDescriptor.edit
.
-
- External Files - 外部文件
-
访问应用的文件存储。
-
实体:
sys$FileDescriptor
. -
界面: External Files 菜单项; (可选)
sys$FileDescriptor.edit
.
-
- Scheduled Tasks - 定时任务
-
创建和管理定时任务。
-
实体:
sys$ScheduledTask
,sys$ScheduledExecution
. -
界面: Scheduled Tasks 菜单项,
sys$ScheduledExecution.browse
,sys$ScheduledTask.edit
.
-
- Entity Inspector - 实体探查
-
从实体探查动态生成的界面中操作应用对象实体。
-
实体: 数据模型需要的实体。
-
界面: Entity Inspector 菜单项,
entityInspector.edit
, 以及数据模型需要的其它实体。
-
- Entity Log - 实体日志
- User Session Log - 用户会话日志
-
查看用户登入登出或者用户会话的历史数据。
-
实体:
sec$SessionLogEntry
. -
界面: User Session Log 菜单项.
-
- Email History - 邮件历史
-
查看从应用发出的电子邮件。
-
实体:
sys$SendingMessage
,sys$SendingAttachment
,sys$FileDescriptor
(邮件附件需要). -
界面: Email History 菜单项,
sys$SendingMessage.attachments
.
-
- Server Log - 服务器日志
-
查看并下载应用的日志文件。
-
实体:
sys$FileDescriptor
. -
界面: Server Log 菜单项,
serverLogDownloadOptionsDialog
. -
特殊功能:
下载日志文件
-
- Screen Profiler - 界面资料
-
应用界面的使用量和使用时间的统计数据。
-
实体:
sec$User
,sys$ScreenProfilerEvent
. -
界面: Screen Profiler 菜单项.
-
- Reports - 报表
-
需要运行报表,参考 报表生成器 插件。
-
实体:
report$Report
,report$ReportInputParameter
,report$ReportGroup
. -
界面:
report$inputParameters
,commonLookup
,report$Report.run
,report$showChart
(如果包含图表模板)。
-
6.3.2. 创建本地管理员
本地管理员可以访问安全子系统的界面,但是他们只能看到自己访问组或以下的用户和访问组。本地管理员还可以创建子访问组、用户以及能分配系统上的角色。其创建的新用户也至少有跟创建他们的本地管理员一样的权限限制。
访问组根节点下面的全局管理员需要创建能被本地管理员看到的角色,然后本地管理员才能分配给用户。本地管理员不能创建或修改角色。
下面是一个访问组结构的例子:
问题有:
-
Departments
下面的用户应该只能看到该组下面的用户,或低于该组层级的组里的用户。 -
Dept 1
,Dept 2
, 这些子组都应该有自己的管理员可以创建用户,分配角色。
方案是:
-
在
Departments
里添加以下约束:-
对于
sec$Group
实体:{E}.id in ( select h.group.id from sec$GroupHierarchy h where h.group.id = :session$userGroupId or h.parent.id = :session$userGroupId )
这样,用户就不会看到比自己所在组层级高的组。
-
对于
sec$User
实体:{E}.group.id in ( select h.group.id from sec$GroupHierarchy h where h.group.id = :session$userGroupId or h.parent.id = :session$userGroupId )
这样,用户不会看到比自己所在组层级高的组里的用户。
-
对于
sec$Role
实体:({E}.description is null or {E}.description not like '[hide]')
这样,用户不会看到
description
属性里包含[hide]
的角色。
-
-
创建一个拒绝编辑角色和权限许可的角色:
-
勾选 Default 默认角色复选框。
-
把
[hide]
字符加到 Description 属性。 -
在 Entities 实体标签页,为
sec$Role
配置拒绝 create, update 和 delete 操作。(给sec$Permission
对象添加权限许可的时候,需要勾选 System level - 系统级 复选框 )。
所有创建的用户,包括本地管理员,会分配
local_user
角色。这个角色对Departments
组的用户不可见,所以本地管理员也不能把这个角色从用户移除。本地管理员只能操作全局管理员给他们创建的角色。显然,department 用户可见的其它角色也不能移除local_user
角色指定的限制。 -
6.4. 集成 LDAP
CUBA 应用程序可以跟 LDAP 集成以便提供以下便利:
-
集中的将用户名和密码保存在 LDAP 数据库。
-
对于 Windows 域用户,可以使用单点登录机制来登录应用系统而不需要提供用户名和密码。
如果启用了 LDAP 集成,用户在应用系统中还是需要一个账号。用户的所有权限和其它属性(除了密码)都保存在应用程序数据库,LDAP 只是用来做用户认证。推荐将大部分用户的应用程序密码留空,除了那些需要标准认证的用户(参考下面)。如果 cuba.web.requirePasswordForNewUsers 属性设置成 false
,那么用户编辑界面不需要显示密码控件。
如果用户的登录名列在 cuba.web.standardAuthenticationUsers 应用程序属性中,应用程序会尝试使用数据库保存的密码哈希值来做用户认证。这样的话,这个列表里的用户可以使用数据的密码登录系统,即便这个用户不在 LDAP 注册过。
基于 CUBA 的应用程序通过 LdapLoginProvider
bean 来跟 LDAP 交互。
可以用 使用 Jespa 集成活动目录 章节描述的 Jespa 类库和相应的 CubaAuthProvider
来启用跟活动目录(Active Directory)的进一步集成,包括使用 Windows 域用户的单点登录。
可以通过自定义的 LoginProvider
,HttpRequestFilter
或者 Web 登录规范描述的事件来实现自定义的登录机制。
还有,可以为 REST API 客户端启用 LDAP 认证:REST API 使用 LDAP 做认证。
6.4.1. 基础 LDAP 集成
如果参数 cuba.web.ldap.enabled 设置为 true
,则启用了 LdapLoginProvider
。 这种情况下,使用 Spring LDAP 类库做用户认证。
下列 Web 客户端应用程序属性用来设置集成 LDAP:
Web 客户端的 local.app.properties 文件示例:
cuba.web.ldap.enabled = true
cuba.web.ldap.urls = ldap://192.168.1.1:389
cuba.web.ldap.base = ou=Employees,dc=mycompany,dc=com
cuba.web.ldap.user = cn=System User,ou=Employees,dc=mycompany,dc=com
cuba.web.ldap.password = system_user_password
在集成了活动目录(Active Directory)的情况下,当使用应用程序来创建用户的时候,用不带域的用户名设置 sAMAccountName
。
6.4.2. 使用 Jespa 集成活动目录
Jespa 是用来集成活动目录和 Java 应用程序的 Java 类库,Jespa 使用 NTLMv2。参考 http://www.ioplex.com 了解更多细节。
6.4.2.1. 引入类库
从 http://www.ioplex.com 下载这个库,然后把这个 JAR 上传到 build.gradle 脚本中注册的一个仓库中。仓库可以是 mavenLocal()
或者内部仓库(私仓)。
在 build.gradle
中的 web 模块配置部分添加以下依赖:
configure(webModule) {
...
dependencies {
compile('com.company.thirdparty:jespa:1.1.17') // from a custom repository
compile('jcifs:jcifs:1.3.17') // from Maven Central
...
在 web 模块创建一个 CubaAuthProvider
的实现类:
package com.company.sample.web;
import com.haulmont.cuba.core.global.AppBeans;
import com.haulmont.cuba.core.global.Configuration;
import com.haulmont.cuba.core.global.GlobalConfig;
import com.haulmont.cuba.core.global.Messages;
import com.haulmont.cuba.core.sys.AppContext;
import com.haulmont.cuba.security.global.LoginException;
import com.haulmont.cuba.web.auth.CubaAuthProvider;
import com.haulmont.cuba.web.auth.DomainAliasesResolver;
import jespa.http.HttpSecurityService;
import jespa.ntlm.NtlmSecurityProvider;
import jespa.security.PasswordCredential;
import jespa.security.SecurityProviderException;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import javax.inject.Inject;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
public class JespaAuthProvider extends HttpSecurityService implements CubaAuthProvider {
private static class DomainInfo {
private String bindStr;
private String acctName;
private String acctPassword;
private DomainInfo(String bindStr, String acctName, String acctPassword) {
this.acctName = acctName;
this.acctPassword = acctPassword;
this.bindStr = bindStr;
}
}
private static Map<String, DomainInfo> domains = new HashMap<>();
private static String defaultDomain;
private Log log = LogFactory.getLog(getClass());
@Inject
private Configuration configuration;
@Inject
private Messages messages;
@SuppressWarnings("deprecation")
@Override
public void init(FilterConfig filterConfig) throws ServletException {
initDomains();
Map<String, String> properties = new HashMap<>();
properties.put("jespa.bindstr", getBindStr());
properties.put("jespa.service.acctname", getAcctName());
properties.put("jespa.service.password", getAcctPassword());
properties.put("jespa.account.canonicalForm", "3");
properties.put("jespa.log.path", configuration.getConfig(GlobalConfig.class).getLogDir() + "/jespa.log");
properties.put("http.parameter.anonymous.name", "anon");
fillFromSystemProperties(properties);
try {
super.init(properties);
} catch (SecurityProviderException e) {
throw new ServletException(e);
}
}
@Override
public void destroy() {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
if (httpServletRequest.getHeader("User-Agent") != null) {
String ua = httpServletRequest.getHeader("User-Agent").toLowerCase();
boolean windows = ua.contains("windows");
boolean gecko = ua.contains("gecko") && !ua.contains("webkit");
if (!windows && gecko) {
chain.doFilter(request, response);
return;
}
}
super.doFilter(request, response, chain);
}
@Override
public void authenticate(String login, String password, Locale loc) throws LoginException {
DomainAliasesResolver aliasesResolver = AppBeans.get(DomainAliasesResolver.NAME);
String domain;
String userName;
int atSignPos = login.indexOf("@");
if (atSignPos >= 0) {
String domainAlias = login.substring(atSignPos + 1);
domain = aliasesResolver.getDomainName(domainAlias).toUpperCase();
} else {
int slashPos = login.indexOf('\\');
if (slashPos <= 0) {
throw new LoginException("Invalid name: %s", login);
}
String domainAlias = login.substring(0, slashPos);
domain = aliasesResolver.getDomainName(domainAlias).toUpperCase();
}
userName = login;
DomainInfo domainInfo = domains.get(domain);
if (domainInfo == null) {
throw new LoginException("Unknown domain: %s", domain);
}
Map<String, String> params = new HashMap<>();
params.put("bindstr", domainInfo.bindStr);
params.put("service.acctname", domainInfo.acctName);
params.put("service.password", domainInfo.acctPassword);
params.put("account.canonicalForm", "3");
fillFromSystemProperties(params);
NtlmSecurityProvider provider = new NtlmSecurityProvider(params);
try {
PasswordCredential credential = new PasswordCredential(userName, password.toCharArray());
provider.authenticate(credential);
} catch (SecurityProviderException e) {
throw new LoginException("Authentication error: %s", e.getMessage());
}
}
private void initDomains() {
String domainsStr = AppContext.getProperty("cuba.web.activeDirectoryDomains");
if (!StringUtils.isBlank(domainsStr)) {
String[] strings = domainsStr.split(";");
for (int i = 0; i < strings.length; i++) {
String domain = strings[i];
domain = domain.trim();
if (!StringUtils.isBlank(domain)) {
String[] parts = domain.split("\\|");
if (parts.length != 4) {
log.error("Invalid ActiveDirectory domain definition: " + domain);
break;
} else {
domains.put(parts[0], new DomainInfo(parts[1], parts[2], parts[3]));
if (i == 0)
defaultDomain = parts[0];
}
}
}
}
}
public String getDefaultDomain() {
return defaultDomain != null ? defaultDomain : "";
}
public String getBindStr() {
return getBindStr(getDefaultDomain());
}
public String getBindStr(String domain) {
initDomains();
DomainInfo domainInfo = domains.get(domain);
return domainInfo != null ? domainInfo.bindStr : "";
}
public String getAcctName() {
return getAcctName(getDefaultDomain());
}
public String getAcctName(String domain) {
initDomains();
DomainInfo domainInfo = domains.get(domain);
return domainInfo != null ? domainInfo.acctName : "";
}
public String getAcctPassword() {
return getAcctPassword(getDefaultDomain());
}
public String getAcctPassword(String domain) {
initDomains();
DomainInfo domainInfo = domains.get(domain);
return domainInfo != null ? domainInfo.acctPassword : "";
}
public void fillFromSystemProperties(Map<String, String> params) {
for (String name : AppContext.getPropertyNames()) {
if (name.startsWith("jespa.")) {
params.put(name, AppContext.getProperty(name));
}
}
}
}
6.4.2.2. 安装和配置
-
按步骤完成 Jespa Operator’s Manual 的 Installation → Step 1: Create the Computer Account for NETLOGON Communication,参考 http://www.ioplex.com/support.html。
-
在 local.app.properties 文件的
cuba.web.activeDirectoryDomains
属性里设置域参数。每个域的描述符应该按照这个格式:domain_name|full_domain_name|service_account_name|service_account_password
。域描述符之间通过分号分隔。示例:
cuba.web.activeDirectoryDomains = MYCOMPANY|mycompany.com|JESPA$@MYCOMPANY.COM|password1;TEST|test.com|JESPA$@TEST.COM|password2
-
启用集成活动目录,通过设置
local.app.properties
文件的cuba.web.externalAuthentication
属性:cuba.web.externalAuthentication = true
-
配置 Jespa 授权 provider,通过设置
local.app.properties
文件的cuba.web.externalAuthenticationProviderClass
属性,这个属性的值是 provider 类的全路径名称(Fully Qualified Name):cuba.web.externalAuthenticationProviderClass = com.company.sample.web.JespaAuthProvider
-
在
local.app.properties
文件配置其它的 Jespa 属性(参考 Jespa Operator’s Manual),示例:jespa.log.level=3
如果应用程序部署在 Tomcat,Jespa 的日志保存在
tomcat/logs
。 -
在浏览器设置添加服务器地址到本地网络:
-
Internet Explorer 和 Chrome: Settings > Security > Local intranet > Sites > Advanced
-
Firefox:
about:config
>network.automatic-ntlm-auth.trusted-uris=http://myapp.mycompany.com
-
6.5. CUBA 应用程序单点登录
CUBA 应用程序单点登录(SSO)允许用户只在浏览器的一个会话输入一次用户名和密码登录之后就可以访问多个运行的应用。
IDP 插件设计用来简化 CUBA 应用程序中的单点登录设置。参阅插件 文档 了解更多细节。
6.6. 社交网站登录
本章节提到的主要是使用 Facebook,Twitter 和 Google+这三个社交网络,依据网络情况,有些网址可能需要科学上网访问。
社交网站登录也是单点登录(SSO) 的一种形式,可以通过社交网站的账号(比如 Facebook,Twitter 或者 Google+)来登录 CUBA 系统,而不需要为 CUBA 应用程序创建特定的账号。
下面将使用 Facebook 来作为社交网络登录的示例。Facebook 使用 OAuth2 认证机制,想了解更多细节请参考 Facebook API 和 Facebook Login Flow: https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow 。
示例项目代码在这里: GitHub,以下列出关键点的实现。
-
为了连接项目到 Facebook,需要创建 App ID (唯一应用程序标识符)和 App Secret (为应用程序项目发送到 Facebook 的请求做认证的一种密码)。按照 介绍 申请,然后在 core 模块的
app.properties
文件中分别以facebook.appId
和facebook.appSecret
这两个属性注册申请到的值。示例:facebook.appId = 123456789101112 facebook.appSecret = 123456789101112abcde131415fghi16
同样也需要配置在 Facebook app 注册的 URL,填写在 core 和 web 模块的应用程序属性文件的 cuba.webAppUrl 参数。示例:
cuba.webAppUrl = http://cuba-fb.test:8080/app
-
扩展 登录界面 添加社交网络登录按钮。这个按钮将调用
loginFacebook()
方法 - 社交网络登录流程的入口。 -
为了使用 Facebook 用户账号,需要在 CUBA 标准的用户账号中添加一个额外字段。扩展
User
实体并添加字符串类型的属性facebookId
:@Column(name = "FACEBOOK_ID") protected String facebookId;
-
创建服务,根据提供的
facebookId
在应用数据库查找用户,然后要么返回已有用户,要么创建新用户:public interface SocialRegistrationService { String NAME = "demo_SocialRegistrationService"; User findOrRegisterUser(String facebookId, String email, String name); }
@Service(SocialRegistrationService.NAME) public class SocialRegistrationServiceBean implements SocialRegistrationService { @Inject private Metadata metadata; @Inject private Persistence persistence; @Inject private Configuration configuration; @Override @Transactional public User findOrRegisterUser(String facebookId, String email, String name) { EntityManager em = persistence.getEntityManager(); TypedQuery<SocialUser> query = em.createQuery("select u from sec$User u where u.facebookId = :facebookId", SocialUser.class); query.setParameter("facebookId", facebookId); query.setViewName(View.LOCAL); SocialUser existingUser = query.getFirstResult(); if (existingUser != null) { return existingUser; } SocialRegistrationConfig config = configuration.getConfig(SocialRegistrationConfig.class); Group defaultGroup = em.find(Group.class, config.getDefaultGroupId(), View.MINIMAL); SocialUser user = metadata.create(SocialUser.class); user.setFacebookId(facebookId); user.setEmail(email); user.setName(name); user.setGroup(defaultGroup); user.setActive(true); user.setLogin(email); em.persist(user); return user; } }
-
创建服务来管理登录过程。本示例中是: FacebookService 包含两个方法:
getLoginUrl()
和getUserData()
。-
getLoginUrl()
生成登录 URL,基于应用程序 URL 和 OAuth2 返回类型(代码、访问令牌(access token)或者两者都有,参考 Facebook API 文档 了解更多返回类型)。这个方法的实现可以参考 FacebookServiceBean.java 文件。 -
getUserData()
使用提供的应用程序 URL 和代码来查找 Facebook 用户,并且返回已有用户的数据或者创建新用户。在这个例子中,希望获取用户的 id,姓名和 email,id 也就是上面创建的facebookId
。
-
-
在 core 模块的
app.properties
文件中定义facebook.fields
和facebook.scope
应用程序属性:facebook.fields = id,name,email facebook.scope = email
-
返回扩展登录窗口控制器的
loginFacebook()
方法。这个控制器的所有代码在 ExtAppLoginWindow.java 文件。在这个方法中,有针对当前会话的请求处理(request handler),保存当前 URL 并且调用重定向到 Facebook 认证表单:
private RequestHandler facebookCallBackRequestHandler = this::handleFacebookCallBackRequest; private URI redirectUri; @Inject private FacebookService facebookService; @Inject private GlobalConfig globalConfig; public void loginFacebook() { VaadinSession.getCurrent() .addRequestHandler(facebookCallBackRequestHandler); this.redirectUri = Page.getCurrent().getLocation(); String loginUrl = facebookService.getLoginUrl(globalConfig.getWebAppUrl(), OAuth2ResponseType.CODE); Page.getCurrent() .setLocation(loginUrl); }
handleFacebookCallBackRequest()
方法会处理 Facebook 认证表单之后的函数回调。首先,使用UIAccessor
实例来锁住 UI 直到登录请求处理完毕。然后,
FacebookService
会获取 facebook 用户账号的 email 和 id。在这之后,相应的 CUBA 用户会通过facebookId
被查找到,或者在此过程中被系统创建。接下来,认证会被触发,这个用户的用户会话会被加载,然后 UI 会更新。之后会移除 Facebook 回调处理,因为此时不再需要认证了。
public boolean handleFacebookCallBackRequest(VaadinSession session, VaadinRequest request, VaadinResponse response) throws IOException { if (request.getParameter("code") != null) { uiAccessor.accessSynchronously(() -> { try { String code = request.getParameter("code"); FacebookUserData userData = facebookService.getUserData(globalConfig.getWebAppUrl(), code); User user = socialRegistrationService.findOrRegisterUser( userData.getId(), userData.getEmail(), userData.getName()); App app = App.getInstance(); Connection connection = app.getConnection(); Locale defaultLocale = messages.getTools().getDefaultLocale(); connection.login(new ExternalUserCredentials(user.getLogin(), defaultLocale)); } catch (Exception e) { log.error("Unable to login using Facebook", e); } finally { session.removeRequestHandler(facebookCallBackRequestHandler); } }); ((VaadinServletResponse) response).getHttpServletResponse(). sendRedirect(ControllerUtils.getLocationWithoutParams(redirectUri)); return true; } return false; }
现在,当用户在登录界面点击 Facebook 按钮时,应用程序会跟用户请求使用 Facebook 账号和邮箱,如果得到用户授权,这个账号登录后会直接跳转到应用程序主界面。
可以通过使用自定义的 LoginProvider
, HttpRequestFilter
或者 Web 登录 章节提到的事件来实现定制化登录机制。
7. Cookbook
本节包含一些实际操作的例子,手册中其它地方没有找到合适的章节来介绍这些例子,也(还)没有包含在 向导 中。
7.1. 建立业务模型
在本节中,可以找到数据模型设计和使用实体属性的方法。
7.1.1. 分配初始值
有多种方式可以为新实体实例的属性分配初始值。
7.1.1.1. 初始化实体字段
简单属性 ( Boolean
、 Integer
等。) 和枚举可以在实体类的相应字段的声明中初始化,参考在 Customer.java 中的 active
和 grade
字段示例。
此外,可以在实体类中创建具有 @PostConstruct 注解的特定初始化方法。此时,在初始化期间可以调用任何全局基础设施接口和 beans,参考在 Customer.java 示例中的 init()
方法。
7.1.1.2. 使用 CreateAction 进行初始化
如果属性的初始值取决于调用界面的数据,可以使用 CreateAction 类的 setInitialValues()
或 setInitialValuesSupplier()
方法。
请参考在演示应用程序中处理 Customer
和 CustomerAddress
实体的示例:
-
customer-address-browse.xml - 包含两个关联表格的界面描述,一个用于客户,另一个用于其地址。
-
CustomerAddressBrowse.java - 界面控制器。在其
init()
方法中,使用setInitialValuesSupplier()
为新建地址的customer
属性提供初始值,设置为第一个表格中当前选中的客户。
7.1.1.3. 使用 initNewItem 方法
初始值也可以在新建实体的界面控制器的 initNewItem() 方法中定义。
考虑如下实体:
在演示应用程序中,CustomerDetails
的属性 info
与 Customer
实体自身的属性在相同的界面上编辑。需要跟所属的 Customer
实例一起创建 CustomerDetails
实例。
-
customer-edit.xml - 客户编辑界面描述。包含一个链接到
CustomerDetails
实例的嵌套数据源。infoField
文本区组件连接到此数据源。 -
CustomerEdit.java - 界面控制器。定义了
initNewItem()
方法,该方法创建了一个新的CustomerDetails
实例并将其设置给新的Customer
。创建的实例可通过嵌套数据源访问,并在提交界面后保存到数据库中。
7.1.2. 组合结构
CUBA 框架支持实体之间的两种关系类型:关联和组合。在 CUBA Studio 界面它们分别被称为 ASSOCIATION 和 COMPOSITION。关联是可以彼此独立存在的对象之间的关系。另外,当明细实例仅能作为主实例的一部分存在时,这种 "主从" 关系可以使用组合而不是关联。比如,机场及其航站楼可以被视为组合关系的实例:不属于任何机场的航站楼没有任何意义。
通常,属于组合关系的实体会被一起编辑,这样更自然。例如,用户打开机场编辑界面并查看航站楼列表,这样用户可以创建和编辑它们,此时,机场和航站楼的所有更改都会在一次事务中一起保存到数据库中,并且仅在用户确认保存主实体后(机场)。
7.1.2.1. 一对多:一级嵌套
让我们以 Airport
和 Terminal
实体实现一对多组合示例:
-
Terminal.java -
Terminal
实体包含指向Airport
的必要链接。在 Studio 实体设计器中,设置
airport
属性: Attribute type - ASSOCIATION,Cardinality - MANY_TO_ONE,Mandatory - ON。 -
Airport.java -
Airport
实体包含一对多的航站楼集合。相应的字段使用 @Composition 注解以实现组合关系,而 @OnDelete 注解用于级联软删除。在 Studio 实体设计器中,设置
terminals
属性: Attribute type - COMPOSITION,Cardinality - ONE_TO_MANY,On delete - CASCADE。 -
views.xml - 机场编辑界面的
airport-terminals
视图包含terminals
集合属性。我们为此属性使用_local
视图,因为Terminal
实体的airport
属性只在创建新的Terminal
实例时设置并且在此之后永远不会改变,所以我们不需要加载它。 -
airport-edit.xml - 机场编辑界面的 XML 描述定义了用于
Airport
实例的数据源和用于其航站楼的嵌套实例,它还包含一个显示航站楼的表格。 -
terminal-edit.xml - 一个
Terminal
实体的标准编辑界面。
因此,机场实例的编辑工作如下:
-
机场编辑界面显示航站楼列表。
-
用户可以选择航站楼并打开其编辑界面。当在航站楼编辑界面中点击 OK 时,航站楼的更新实例不会保存到数据库,而是保存到机场编辑界面的
terminalsDs
数据源。 -
用户可以创建新的航站楼和删除现有航站楼。所有改变都将保存到
terminalsDs
数据源。 -
当用户在机场编辑界面点击 OK 时,更新的
Airport
实例与所有更新的Terminal
实例将一起被提交到中间层的 DataManager.commit() 方法并使用单个事务保存到数据库中。
7.1.2.2. 一对多:二级嵌套
组合关系可以更深,但是最多到两个嵌套级别,我们通过添加用于描述航站楼汇合点的 MeetingPoint
实体来扩展前一个示例:
Terminal
实体包含 meetingPoints
属性 – 一个 MeetingPoint
实例的集合。为了使所有三个实体成为一个组合并一起编辑,除了之前描述的步骤外,还应完成以下步骤:
-
Terminal.java -
Terminal
类的meetingPoints
属性带有@Composition
和@OnDelete
注解,跟Airport
类的terminals
属性类似。 -
views.xml -
Terminal
类的terminal-meetingPoints-view
视图包含meetingPoints
集合属性。该视图在Airport
实体的airport-terminals-meetingPoints-view
视图中使用。 -
airport-edit.xml -
Airport
编辑界面 XML 描述,包含Airport
的实例的数据源和整个组合(airportDs
>terminalsDs
>meetingPointsDs
)的嵌套实体。这里,
meetingPointsDs
数据源不与任何可视化组件关联,但是用于正确处理组合的联合编辑。 -
terminal-edit.xml - 航站楼编辑界面 XML 描述,包含嵌套数据源以及对应于
meetingPoints
集合的表格。
这样,更新的 MeetingPoint
实例以及 Terminal
实例将仅与 Airport
实例在同一事务中一起保存到数据库。
7.1.2.3. 一对多:三级嵌套
假设需要添加包含汇合点的一些细节信息的附加实体:Note。现在整个结构如下: Airport > Terminal > Meeting Point > Note。
CUBA 可以处理最多两层嵌套的组合。这里我们有三个级别,所以我们应该从顶部或底部限制深度。下面我们考虑从组合中移除机场的两种不同方法(从用户体验的角度来看)。此时有个问题,由于移除机场后,航站楼将会独立于机场保存到数据库中,因此无法为尚未保存到数据库的新建机场保存航站楼。两种方法都能解决这个问题:
-
在第一种方法中,机场浏览界面和编辑界面看起来与上面相同,但是编辑界面有额外的 Save 按钮用来保存新机场而不关闭界面。在保存新机场前,用户无法为新机场创建航站楼。
-
airport-edit.xml 包含航站楼的独立数据源,而不是嵌套数据源。该独立数据源链接到机场数据源,这样可以为被编辑的机场加载航站楼。此外,机场编辑界面包含
extendedEditWindowActions
界面子框架,允许用户保存机场而不关闭界面。 -
AirportEdit.java - 这里,在机场编辑界面的
postInit()
方法内,我们控制航站楼的 Create 操作是否启用,然后传递当前机场实例用来初始化新建航站楼的机场属性。
-
-
在第二种方法中,我们将机场浏览界面拆分为两个面板:一个用于机场列表,另一个用于展示该机场依赖的航站楼列表。航站楼列表现在在机场编辑界面之外。选择机场后,才会启用航站楼的 Create 操作。
-
airport-browse.xml 包含航站楼列表的独立数据源。它链接到机场数据源,这样可以为所选机场加载航站楼。
-
AirportBrowse.java - 在机场浏览界面控制器的
init()
方法中,控制航站楼的 Create 操作是否启用,然后传递当前机场实例用来初始化新建航站楼的机场属性。
-
7.1.2.4. 一对一组合
一对一组合将通过 Customer
和 CustomerDetails
实体说明:
-
Customer.java -
Customer
实体包含一个到CustomerDetails
实体的引用属性,并且使用了@Composition
注解。 -
CustomerDetails.java -
CustomerDetails
实体。 -
customer-edit.xml - 客户编辑界面描述。包含指向
CustomerDetails
实例的嵌套数据源。为了加载嵌套实例,根数据源使用了一个包含details
属性的Customer
实体的视图。客户编辑界面中的字段组仅定义一个用于显示details
属性的字段。
这样,客户编辑的工作方式如下:
-
客户编辑界面包含 PickerField 组件,其中包含两个操作: OpenAction 和 ClearAction:
-
当调用打开操作时,会创建一个新的
CustomerDetails
实例并显示其编辑界面。在详细信息编辑界面中点击 OK 时,详细信息实例不会保存到数据库中,而是保存到客户编辑界面的detailsDs
数据源中。 -
选择器字段显示细节实体的实例名。
-
当用户在客户编辑界面中点击 OK 时,更新后的
Customer
实例与CustomerDetails
实例一起被提交到中间层的DataManager.commit()
方法并使用单个事务保存到数据库中。 -
如果用户调用选择器字段的清除操作,则会删除
CustomerDetails
实例,并在用户提交客户编辑界面后的同一事务中清除对该实例的引用。
7.1.2.5. 用单一编辑界面处理一对一组合
在一个编辑界面中编辑 One-to-One 组合通常很方便。我们看看以下示例如何实现编辑 Customer 和 CustomerDetails 一对一关系。
-
customer-edit.xml 描述包含主数据源
customerDs
和嵌套数据源detailsDs
:<dsContext> <datasource id="customerDs" class="sample.entity.Customer" view="customer-view"> <datasource id="detailsDs" property="details"/> </datasource> </dsContext>
两个实体需要编辑的所有字段都被分到同一个
fieldGroup
,其中一些字段被绑定到嵌套数据源:<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>
-
在 CustomerEdit 控制器中重写
initNewItem()
方法,创建一个CustomerDetails
实例,并将此示例关联到刚创建的Customer
实例:@Inject private Metadata metadata; @Override protected void initNewItem(Customer customer) { customer.setDetails(metadata.create(CustomerDetails.class)); }
最后,处理用户单击 Create 然后想要关闭编辑界面而不修改任何内容的情况。该用户将被要求保存更改,因为
detailsDs
数据源已经包含一个空实例,AbstractEditor
的isModified()
方法将始终返回true
。为避免出现确认对话框,应该让isModified()
方法仅关注主数据源中的更改:@Override public boolean isModified() { return customerDs.isModified(); }
现在,可以在一个编辑界面界面中创建和编辑两个关联的实体。
7.2. 使用数据库
本节提供有关在应用程序开发环境和生产环境使用数据库的实用建议。
有关使用特定 DBMS 的配置参数的信息,请参阅数据库组件部分。
7.2.1. 创建数据库架构
在应用程序开发过程中,需要创建和维护与模型实体对应的数据库架构。平台提供了一种基于数据库创建和更新脚本的方法来完成此任务。下面介绍使用这种方法的实际步骤。
创建和维护数据库架构的任务包括两部分:创建脚本和执行脚本。
可以手动创建脚本,也可以使用 Studio 创建脚本。下面介绍在 Studio 中创建脚本的过程。在 CUBA 主菜单或者 Data Model 右键菜单中运行 Generate Database Scripts 命令。这时,Studio 会连接到 Project Properties 窗口中定义的数据库,并将可用的 DB 架构与当前数据模型进行比较。
如果数据库不存在或没有 SYS_DB_CHANGELOG 和 SEC_USER 表,则系统仅生成数据库初始化脚本。否则,也会创建更新脚本。脚本生成后,会打开含有生成脚本的窗口。
更新脚本显示在 Updates 标签页上。状态是 new 的脚本反映数据模型的当前状态与 DB 架构之间的差异。对每个新表或修改的表会创建单独的脚本。某些脚本还包含一组参照完整性约束。单击 Save and close 关闭界面时,脚本将保存在 core 模块的 db/update/{db_type}
目录中。
项目中存在并且已应用到 DB 的脚本将显示为 applied 状态。这种脚本不能被修改或删除。
Updates 标签页还会显示出 to be deleted 状态的脚本。这些脚本是项目中有效脚本,但没有应用到 DB。单击 Save and close 关闭界面时,这些脚本会被删除。如果脚本由之前的脚本生成过程创建,但没有通过调用 Update Database 将脚本应用到 DB,这是标准行为。在这种情况下,不再需要这些脚本,因为当前数据库架构和数据模型之间的差异反映在最新生成的脚本中。但是,如果脚本由其它开发人员编写并从版本控制系统获取,则应取消保存并先应用这些脚本到数据库,然后再生成新脚本。
Init Tables 、 Init Constraints 和 Init Data 标签页中显示位于 core 模块的 db/init/{db_type}
目录中的 DB 创建脚本。
Init Tables 标签页显示用于创建表的 10.create-db.sql
脚本。同一个表相关的代码由 begin {table_name} ... end {table_name}
注释包围起来。当模型中的实体发生更改时,Studio 仅替换注释之间(begin … end 之间)的相应表的代码,而保留其余代码。这样,如果这个文件中有手工编写的代码,也不会被覆盖。因此,请勿在手工编辑代码时删除这些注释,否则 Studio 将无法将更改正确地应用到现有文件。
Init Constraints 标签页显示用于创建完整性约束的 20.create-db.sql
脚本。同样,它也有不应被删除的用于分隔表的注释。
Init Data 标签页显示 30.create-db.sql
脚本,该脚本用于在初始化 DB 时提供额外信息。这些额外的信息可以是函数、触发器、DML 操作或必要的数据库初始数据。如有必要,可手工创建此脚本的内容。
在应用程序开发的初始阶段,数据模型更改比较频繁,建议仅使用 DB 创建脚本(位于 Init Tables、Init Constraints、Init Data 标签页中)并在调用 Generate DB scripts 命令后立即移除 Updats 标签页中的更新脚本。这是使数据库保持最新状态的最简单可靠的方法。当然,它有一个明显的缺点,那就是应用这些脚本会从头开始重新创建数据库,所有数据都会丢失。在开发阶段可以通过向 Init Data 脚本添加命令(填充数据的脚本)从一定程度上弥补这一缺陷,该脚本可以在初始化时填充一些基本的数据。 更新脚本会成为后期开发和维护数据库的方便又必要的工具,当数据模型相对稳定时,开发和生产数据库中的数据不会因为从头开始重新创建数据库而丢失。 |
使用Gradle 任务执行数据库脚本来应用脚本:调用 CUBA > Create Database 重新创建数据库,调用 CUBA > Update Database 应用脚本;Data Model 的右键菜单也有同样的命令。请注意,只有应用程序服务停止时,这些操作才可用。当然,也可以随时从命令行调用相应的 Gradle 任务( createDb
和 updateDb
),但如果数据库或其任何数据库对象被锁定,脚本执行可能会失败。
7.2.2. MS SQL Server 特性
Microsoft SQL Server 使用表的聚集索引。
默认情况下,聚集索引以表的主键为基础,但是,CUBA 应用程序使用的 UUID
类型的键不适合聚集索引。建议使用 nonclustered
修饰符创建 UUID 主键:
create table SALES_CUSTOMER (
ID uniqueidentifier not null,
CREATE_TS datetime,
...
primary key nonclustered (ID)
)^
7.2.3. Oracle 数据库特性
由于 Oracle JDBC 驱动程序的分发策略限制,只能从 http://www.oracle.com/technetwork/database/features/jdbc/index-091264.html 手动下载。
下载后,将 ojdbc6.jar
文件复制到 CUBA Studio 安装路径的 lib
子目录和已安装的 Tomcat 服务的 lib
子目录。然后停止 Studio,通过在命令行中执行
gradle --stop
来停止 Gradle 守护进程,然后重启 Studio。
对于 SE 版本,将 JAR 文件复制到 ~/.haulmont/studio/lib/
目录和已安装的 Tomcat 服务的 lib
子目录。
7.2.4. MySQL 数据库特性
由于许可限制,MySQL JDBC 驱动程序不随 CUBA Studio 一起分发,因此应该执行以下操作:
-
从 https://dev.mysql.com/downloads/connector/j 下载驱动程序存档文件
-
解压缩 JAR 文件并将其重命名为
mysql-connector-java.jar
-
将 JAR 文件复制到
Program Files/Haulmont/CUBA Studio 2018.3/plugins/cuba-studio/lib/
目录以及已安装的 Tomcat 服务的lib
子目录 -
通过在命令行中执行
gradle --stop
停止 Gradle 守护进程,然后再次启动 Studio
对于不使用 UTF-8 字符集的数据库,不能执行框架中带有约束的数据脚本。此时需要修改数据库的 Charset 和 Collation name 属性,可以通过在项目属性的 Connection params 字段传递以下参数实现:
|
MySQL 不支持部分索引(partial indexes),因此为支持软删除的实体实现唯一约束的唯一方法是在索引中使用 DELETE_TS
列。但是还有另一个问题:MySQL 在具有唯一约束的列中允许存在多个 NULL 值。由于标准 DELETE_TS
列可以为 null,因此它不能在唯一索引中使用。建议使用以下变通方案为支持软删除的实体创建唯一约束:
-
在数据库表中创建一个
DELETE_TS_NN
列。此列不为空,并被初始化为默认值:create table DEMO_CUSTOMER ( ... DELETE_TS_NN datetime(3) not null default '1000-01-01 00:00:00.000', ... )
-
创建一个触发器,当更改
DELETE_TS
值时将更改DELETE_TS_NN
值:create trigger DEMO_CUSTOMER_DELETE_TS_NN_TRIGGER before update on DEMO_CUSTOMER for each row if not(NEW.DELETE_TS <=> OLD.DELETE_TS) then set NEW.DELETE_TS_NN = if (NEW.DELETE_TS is null, '1000-01-01 00:00:00.000', NEW.DELETE_TS); end if
-
创建一个唯一索引,包括唯一列和
DELETE_TS_NN
:create unique index IDX_DEMO_CUSTOMER_UNIQ_NAME on DEMO_CUSTOMER (NAME, DELETE_TS_NN)
7.2.5. 连接到非默认数据库架构
PostgreSQL 和 Microsoft SQL Server 支持连接到指定的数据库架构。默认情况下,PostgreSQL 会连接到 public
,SQL Server 会连接到 dbo
。
PostgreSQL
task createDb(dependsOn: assembleDbScripts, type: CubaDbCreation) {
dbms = 'postgres'
host = 'localhost'
dbName = 'my_db'
connectionParams = '?currentSchema=my_schema'
dbUser = 'cuba'
dbPassword = 'cuba'
}
如果正在使用 Studio,请将此连接参数添加到 Project properties 窗口的 Connection params 字段。Studio 会自动更新 build.gradle
。之后,可以更新或重新创建数据库,所有表将在指定的架构下创建。
Microsoft SQL Server
在 Microsoft SQL Server 上,仅提供连接属性是不够的,必须将架构与数据库用户相关联。下面是创建一个新数据库并使用非默认架构的示例。
-
创建一个登录用户:
create login JohnDoe with password='saPass1'
-
创建一个新数据库:
create database my_db
-
以
sa
身份连接到新数据库,创建架构,然后创建用户并授予其所有者权限:create schema my_schema create user JohnDoe for login JohnDoe with default_schema = my_schema exec sp_addrolemember 'db_owner', 'JohnDoe'
现在,应该在 updateDb Gradle 任务(或 Studio 项目属性)的 connectionParams
属性中指定 currentSchema
参数。实际上,此属性不是由 SQL Server JDBC 驱动程序处理的,但它会告诉 Studio 和 CUBA Gradle 插件使用什么架构。
task updateDb(dependsOn: assembleDbScripts, type: CubaDbUpdate) {
dbms = 'mssql'
dbmsVersion = '2012'
host = 'localhost'
dbName = 'my_db'
connectionParams = ';currentSchema=my_schema'
dbUser = 'JohnDoe'
dbPassword = 'saPass1'
}
请注意,由于 SQL Server 的特性 - 非默认架构需要与用户关联,所以无法从 Studio 或在命令行中执行 createDb
来重新创建 SQL Server 数据库。但是,如果在 Studio 中运行 Update database 或在命令行中运行 updateDb
,则现有数据库中指定架构下所有必须的表都会被创建。
7.2.6. 在生产环境中创建和更新数据库
本节介绍在应用程序部署和运行期间创建和更新数据库的几种方法。要了解有关数据库脚本结构和规则的更多信息,请参阅创建和更新数据库的脚本和创建数据库架构。
7.2.6.1. 在服务器上执行数据库脚本
服务器执行数据脚本机制可用于初始化数据库及后续对应用程序开发期间发生的数据库架构调整进行更新。
这里的“服务器”指的是生产环境的服务器 |
按照以下操作完成对新数据库的初始化:
-
通过将以下行添加到中间件 block 的local.app.properties文件中以启用cuba.automaticDatabaseUpdate应用程序属性:
cuba.automaticDatabaseUpdate = true
对于快速部署的 Tomcat,该文件位于
tomcat/conf/app-core
目录中。如果该文件不存在,请创建它。 -
创建一个与context.xml中的数据源描述中指定的 URL 对应的空数据库。
-
启动包含中间件 block 的应用程序服务。在应用程序启动时,数据库将被初始化并准备就绪。
之后,每次应用程序服务启动时,脚本执行机制都会将位于数据库脚本目录中的脚本与在数据库中注册的已执行脚本列表进行比较。如果找到新脚本,新脚本将被执行并注册。典型情况下,在每个新的应用程序版本中包含更新脚本就足够了,数据库会在每次应用程序重新启动的时候进行更新。
在服务启动时使用数据库脚本执行机制时,应考虑以下事项:
-
如果在运行脚本时发生任何错误,则中间件 block 将停止初始化并变得不可用。客户端 block 会生成关于无法连接到中间件的错误提示消息。
检查位于服务器日志文件夹中的 app.log 文件,从
com.haulmont.cuba.core.sys.DbUpdaterEngine
日志中获取关于 SQL 执行的消息,可能会有其它可以用来识别错误原因的错误消息。 -
更新脚本和脚本中用
"^"
分隔的 DDL 和 SQL 命令一样在单独的事务中执行。这就是为什么当更新失败时,仍然很有可能一部分脚本甚至最后一个脚本的个别命令已被执行并提交给数据库。考虑到这一点,强烈建议在启动服务之前创建数据库的备份。然后,更新脚本的错误得到修复时,可以恢复数据库并重新进行数据库更新。
如果没有进行备份,则应在错误修复后确定脚本的哪些部分已被执行并已提交。如果整个脚本执行失败,则简单地重启服务并运行自动更新即可。如果错误出现之前的一些用
"^"
字符分隔的命令已在单独的事务中执行并且提交,这种情况下只需运行脚本中剩余未执行的命令,同时手动在 SYS_DB_CHANGELOG 中注册这个手动执行的脚本。之后,可以启动服务,自动更新机制将继续处理下一个未执行的脚本。CUBA Studio 为所有数据库类型生成带有 ";" 分隔符的更新脚本,除了 Oracle。如果更新脚本命令由分号分隔,则脚本在一个事务中执行,并在发生错误时完全回滚。此行为可确保数据库架构与已执行的更新脚本列表之间的一致性。
7.2.6.2. 从命令行初始化和更新数据库
可以使用平台的中间层 block 中包含的 com.haulmont.cuba.core.sys.utils.DbUpdaterUtil
类通过命令行运行数据库创建和更新脚本。在启动时,应指定以下参数:
-
dbType
– 数据库类型,可选值: postgres、mssql、oracle、mysql。 -
dbVersion
– DBMS 版本 (可选参数)。 -
dbDriver
- JDBC 驱动程序类名(可选参数)。如果没有提供,将根据dbType
确定合适的驱动程序类名。 -
dbUser
– 数据库用户名。 -
dbPassword
– 数据库用户密码 -
dbUrl
– 连接数据库的 URL。对数据库的初始化来说,这里指定的数据库应该是空库,初始化程序不会自动清空数据库。 -
scriptsDir
– 以标准目录结构包含脚本的文件夹的绝对路径。通常,这是应用程序提供的数据库脚本目录。 -
可用的命令:
-
create
– 初始化数据库。 -
check
– 显示所有未执行的更新脚本。 -
update
– 更新数据库。
-
运行 DbUpdaterUtil
的 Linux 脚本示例:
#!/bin/sh
DB_URL="jdbc:postgresql://localhost/mydb"
APP_CORE_DIR="./../webapps/app-core"
WEBLIB="$APP_CORE_DIR/WEB-INF/lib"
SCRIPTS="$APP_CORE_DIR/WEB-INF/db"
TOMCAT="./../lib"
SHARED="./../shared/lib"
CLASSPATH=""
for jar in `ls "$TOMCAT/"`
do
CLASSPATH="$TOMCAT/$jar:$CLASSPATH"
done
for jar in `ls "$WEBLIB/"`
do
CLASSPATH="$WEBLIB/$jar:$CLASSPATH"
done
for jar in `ls "$SHARED/"`
do
CLASSPATH="$SHARED/$jar:$CLASSPATH"
done
java -cp $CLASSPATH com.haulmont.cuba.core.sys.utils.DbUpdaterUtil \
-dbType postgres -dbUrl $DB_URL \
-dbUser $1 -dbPassword $2 \
-scriptsDir $SCRIPTS \
-$3
此脚本用于与本地 PostgreSQL 服务器上运行的名为 mydb
的数据库一起使用。此脚本应位于 Tomcat 服务的 bin
文件夹中,并且应该以 {username}
、 {password}
和 {command}
参数启动,例如:
./dbupdate.sh cuba cuba123 update
脚本执行进度显示在控制台中。如果发生任何错误,则上一章节中针对自动更新机制所描述的操作在这里同样适用。
从命令行更新数据库时,会启动现有的 Groovy 脚本,但只会执行其主要部分。由于缺少服务的上下文,脚本的 |
7.2.7. 使用 Squirrel SQL 连接 HSQLDB
HSQLDB,也称为 HyperSQL,是一种简单方便的 DBMS,一般用于应用程序原型,这种数据库不需要安装,并且如果项目已配置为使用此 DBMS,那么它会在 CUBA Studio 中自动启动。本节介绍使用外部工具连接到 HSQLDB 的方法,该工具允许直接通过 SQL 处理数据库结构和数据。
SquirreL SQL Client 是一个开源 Java 应用程序,能够通过 JDBC 处理数据库。可以从这里下载 Squirrel SQL: http://squirrel-sql.sourceforge.net 。
在启动 Squirrel SQL 之前,在 CUBA Studio 安装路径的 lib
文件夹中找到 hsqldb-x.x.x.jar
文件,并将其复制到 Squirrel SQL 的 lib
文件夹中。
启动 Squirrel SQL 并定位到 Drivers。确保 HSQLDB Server 驱动处于活动状态。
打开 Aliases 标签页,然后单击 Create a new Alias 按钮。
在出现的窗口中填写连接属性:数据库 URL、用户名和密码。默认用户名为 sa
,密码为空。可以从 CUBA 项目视图中的 Project properties 部分或应用程序项目的 modules/core/web/META-INF/context.xml
文件中复制数据库 URL。
7.3. 获取本地化消息
本节介绍在应用程序的不同部分中获取本地化消息的方法。
-
在界面 XML 描述中,用于显示静态文本的组件属性(例如 caption)可以使用MessageTools.loadString()方法的规则来处理本地化消息。例如:
-
caption ="msg://roleName"
- 获取当前界面的消息包中roleName
键定义的消息。界面消息包由 XML 描述中根window
元素的messagesPack
属性定义。 -
caption ="msg://com.company.sample.entity/Role.name"
- 获取由com.company.sample.entity
消息包中的Role.name
键定义的消息。
-
-
在界面控制器中,可以通过以下方式检索本地化字符串:
-
从当前界面消息包:
-
使用继承自 AbstractFrame 基类的
getMessage()
方法。例如:
-
String msg = getMessage("warningMessage");
-
使用继承自
AbstractFrame
基类的formatMessage()
方法。此时,按照String.format()
方法定义的规则使用提取的消息对提交的参数进行格式化。例如:messages.properties:
warningMessage = Invalid email address: '%s'
Java 控制器:
String msg = formatMessage("warningMessage", email);
-
使用注入的 Messages 基础接口从各种消息包获取消息。例如:
@Inject private Messages messages; @Override public void init(Map<String, Object> params) { String msg = messages.getMessage(getClass(), "warningMessage"); ... }
-
-
-
对于由 Spring 容器管理的组件(托管 bean,services,JMX-beans,portal 模块的 Spring MVC 控制器),本地化消息可以使用注入的Messages 基础接口进行检索:
@Inject protected Messages messages; ... String msg = messages.getMessage(getClass(), "warningMessage");
portal 模块中 Thymeleaf 模板的本地化消息也可以通过 portal 模块的主消息包中的消息键获得:
模板<h1 th:text="#{messageKey}"></h1>
portal 主消息包messageKey = Localized message
-
在无法进行注入的应用程序代码中,可以使用
AppBeans
类的静态get()
方法获取Messages
接口:protected Messages messages = AppBeans.get(Messages.class); ... String msg = messages.getMessage(getClass(), "warningMessage");
7.4. 加载和显示图片
我们看看加载、存储和显示员工图片的任务:
-
员工由
Employee
实体表示。 -
图片文件存储在 FileStorage 中。
Employee
实体包含指向相应FileDescriptor
的链接。 -
Employee
编辑界面显示图片,还支持上传,下载和清除图片。
带有图片文件链接的实体类:
@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;
}
}
同时加载 Employee
和 FileDescriptor
的视图需要包含 FileDescriptor
的所有本地属性:
<view entity="demo_Employee"
name="employee-edit"
extends="_local">
<property name="imageFile"
view="_local"/>
</view>
Employee
编辑界面 XML 描述的一个片段:
<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>
现在,现在我们来看看编辑界面控制器。
import com.company.sales.entity.Employee;
import com.haulmont.cuba.core.entity.FileDescriptor;
import com.haulmont.cuba.core.global.DataManager;
import com.haulmont.cuba.core.global.FileStorageException;
import com.haulmont.cuba.gui.Notifications;
import com.haulmont.cuba.gui.components.Button;
import com.haulmont.cuba.gui.components.FileDescriptorResource;
import com.haulmont.cuba.gui.components.FileUploadField;
import com.haulmont.cuba.gui.components.Image;
import com.haulmont.cuba.gui.export.ExportDisplay;
import com.haulmont.cuba.gui.export.ExportFormat;
import com.haulmont.cuba.gui.model.InstanceContainer;
import com.haulmont.cuba.gui.screen.*;
import com.haulmont.cuba.gui.upload.FileUploadingAPI;
import javax.inject.Inject;
@UiController("sales_Employee.edit")
@UiDescriptor("employee-edit.xml")
@EditedEntityContainer("employeeDc")
@LoadDataBeforeShow
public class EmployeeEdit extends StandardEditor<Employee> {
@Inject
private InstanceContainer<Employee> employeeDc;
@Inject
private Image image;
@Inject
private FileUploadField uploadField;
@Inject
private FileUploadingAPI fileUploadingAPI;
@Inject
private DataManager dataManager;
@Inject
private Notifications notifications;
@Inject
private Button downloadImageBtn;
@Inject
private Button clearImageBtn;
@Subscribe
protected void onInit(InitEvent event) { (1)
uploadField.addFileUploadSucceedListener(uploadSucceedEvent -> {
FileDescriptor fd = uploadField.getFileDescriptor(); (2)
try {
fileUploadingAPI.putFileIntoStorage(uploadField.getFileId(), fd);
} catch (FileStorageException e) {
throw new RuntimeException("Error saving file to FileStorage", e);
}
getEditedEntity().setImageFile(dataManager.commit(fd)); (3)
displayImage(); (4)
});
uploadField.addFileUploadErrorListener(uploadErrorEvent ->
notifications.create()
.withCaption("File upload error")
.show());
employeeDc.addItemPropertyChangeListener(employeeItemPropertyChangeEvent -> { (5)
if ("imageFile".equals(employeeItemPropertyChangeEvent.getProperty()))
updateImageButtons(employeeItemPropertyChangeEvent.getValue() != null);
});
}
@Subscribe
protected void onAfterShow(AfterShowEvent event) { (6)
displayImage();
updateImageButtons(getEditedEntity().getImageFile() != null);
}
public void onDownloadImageBtnClick() { (7)
if (getItem().getImageFile() != null)
exportDisplay.show(getItem().getImageFile(), ExportFormat.OCTET_STREAM);
}
public void onClearImageBtnClick() { (8)
getEditedEntity().setImageFile(null);
displayImage();
}
private void updateImageButtons(boolean enable) {
downloadImageBtn.setEnabled(enable);
clearImageBtn.setEnabled(enable);
}
private void displayImage() { (9)
if (getEditedEntity().getImageFile() != null) {
image.setSource(FileDescriptorResource.class).setFileDescriptor(getEditedEntity().getImageFile());
image.setVisible(true);
} else {
image.setVisible(false);
}
}
}
1 | onInit() 方法首先初始化用于上传新图片的 uploadField 组件。 |
2 | 在成功上传的情况下,从组件获取新的 FileDescriptor 实例,并通过调用 FileUploadingAPI.putFileIntoStorage() 将相应的文件从临时客户端存储发送到 FileStorage 。 |
3 | 之后,通过调用 DataManager.commit() 将 FileDescriptor 保存到数据库中,并将保存的实例分配给 Employee 实体的 imageFile 属性。 |
4 | 然后,调用控制器的 displayImage() 方法显示上传的图片。 |
5 | 之后,将一个监听器添加到包含 Employee 实例的数据容器中。监听器根据文件加载的状态启用或禁用下载和清除按钮。 |
6 | onAfterShow() 方法根据加载的文件是否存在来决定是否显示图片并且更新按钮状态。 |
7 | 点击 downloadImageBtn 按钮时调用 onDownloadImageBtnClick() ,使用 ExportDisplay 接口下载文件。 |
8 | 点击 clearImageBtn 时调用 onClearImageBtnClick() ;它只是清除了 Employee 实体的 imageFile 属性。该文件不会从存储中删除。 |
9 | displayImage() 方法从存储中加载文件并设置 image 组件的内容。 |
7.4.1. 在表格列中显示图片
为了扩展上一个任务,我们在 Employee
浏览界面上将图片作为员工头像添加到表格中。
图片可以单独显示在某列,也可以跟其它内容一起显示在现有的列中。这两种情况都使用 Table.ColumnGenerator 接口。
下面是 Employee
浏览界面 XML 描述的一个片段:
<groupTable id="employeesTable"
width="100%"
dataContainer="employeesDc">
<actions>
<action id="create" type="create"/>
<action id="edit" type="edit"/>
<action id="remove" type="remove"/>
</actions>
<columns>
<column id="name"/>
</columns>
<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>
要在 name
列中将员工姓名与图片显示在一行,我们要修改此列中数据的标准展示方式。将使用 HBoxLayout 容器并将 Image 组件放入其中:
import com.company.demo.entity.Employee;
import com.haulmont.cuba.core.entity.FileDescriptor;
import com.haulmont.cuba.gui.UiComponents;
import com.haulmont.cuba.gui.components.*;
import com.haulmont.cuba.gui.screen.LookupComponent;
import com.haulmont.cuba.gui.screen.*;
import javax.inject.Inject;
@UiController("sales_Employee.browse")
@UiDescriptor("employee-browse.xml")
@LookupComponent("employeesTable")
@LoadDataBeforeShow
public class EmployeeBrowse extends StandardLookup<Employee> {
@Inject
private UiComponents uiComponents;
@Inject
private GroupTable<Employee> employeesTable;
@Subscribe
protected void onInit(InitEvent event) { (1)
employeesTable.addGeneratedColumn("name", entity -> {
Image image = uiComponents.create(Image.NAME); (2)
image.setScaleMode(Image.ScaleMode.CONTAIN);
image.setHeight("40");
image.setWidth("40");
FileDescriptor imageFile = entity.getImageFile(); (3)
image.setSource(FileDescriptorResource.class)
.setFileDescriptor(imageFile);
Label userLogin = uiComponents.create(Label.NAME); (4)
userLogin.setValue(entity.getName());
userLogin.setAlignment(Component.Alignment.MIDDLE_LEFT);
HBoxLayout hBoxLayout = uiComponents.create(HBoxLayout.NAME); (5)
hBoxLayout.setSpacing(true);
hBoxLayout.add(image);
hBoxLayout.add(userLogin);
return hBoxLayout;
});
}
}
1 | onInit() 方法调用 addGeneratedColumn() 方法,此方法接收两个参数:列标识符和 Table.ColumnGenerator 接口的实现。后者用于在 name 列中定义数据的自定义显示方式。 |
2 | 在这个方法中,我们使用 UiComponents 接口创建一个 Image 组件,设置了组件的缩放模式(设置为 CONTAIN ) 及其尺寸参数。 |
3 | 然后获取存储在 File Storage 中图片的 FileDescriptor 实例。该图片的链接存储在 Employee 实体的 imageFile 属性中。FileDescriptorImageResource 资源类型用于设置 Image 组件的资源。 |
4 | 我们将在图片旁边的 Label 组件中显示 name 属性。 |
5 | 我们将 Image 和 Label 组件包装到 HBoxLayout 容器,并使 addGeneratedColumn() 方法返回此容器作为新的表格单元格布局。 |
另外,也可以使用 XML 的generator 属性以更具“声明式”的方式进行定义。
Appendix A: 项目配置文件
本附录介绍 CUBA 应用程序中包含的主要配置文件。
A.1. app-component.xml
app-component.xml
文件应该被放在一个包内,通过 global 模块 JAR 包组装清单文件(manifest)的 App-Component-Id
记录来指定。这个组装文件的记录使得构建系统能从构建 classpath 中查找需要的项目的组件。因此,如果需要在项目中使用某些组件,只需要在 build.gradle 文件中的 dependencies/appComponent
条目定义组件的 global 工件即可。
按照惯例,app-component.xml
在项目的包名根路径(root package)(定义在 metadata.xml),这个根路径跟 build.gradle 中定义的项目工件的组名称一样:
App-Component-Id == root-package == cuba.artifact.group == e.g. 'com.company.sample'
使用 CUBA Studio 来自动生成 app-component.xml
文件和当前项目的组装清单记录。
- 用第三方依赖作为 appJars:
-
如果需要被引入组件的第三方依赖跟应用程序的工件(
app-comp-core
或者app-comp-web
)一起部署到tomcat/webapps/app[-core]/WEB-INF/lib/
目录,需要将这些第三方依赖作为 appJar 类库添加:<module blocks="core" dependsOn="global,jm" name="core"> <artifact appJar="true" name="cuba-jm-core"/> <artifact classifier="db" configuration="dbscripts" ext="zip" name="cuba-jm-core"/> <!-- Specify only the artifact name for your appJar 3rd party library --> <artifact name="javamelody-core" appJar="true" library="true"/> </module>
如果不希望将项目作为 app 组件使用,需要在
build.gradle
的 deploy 任务中将这些依赖作为 appJars 添加:configure(coreModule) { //... task deploy(dependsOn: assemble, type: CubaDeployment) { appName = 'app-core' appJars('app-global', 'app-core', 'javamelody-core') } //... }
A.2. context.xml
context.xml
文件是应用程序部署到 Apache Tomcat 服务描述文件。在部署的程序中,这个文件在 web 应用程序目录或者 WAR 文件的 META-INF
目录,比如 tomcat/webapps/app-core/META-INF/context.xml
。在应用程序项目中,这个文件在 core, web 和 portal 模块的 /web/META-INF
目录。
Middleware block 中,此文件的主要目的是用来定义使用 JNDI 名称的 JDBC 数据源,JNDI 名称定义在 cuba.dataSourceJndiName 应用程序属性中。
PostgreSQL 数据源定义示例:
<Resource
name="jdbc/CubaDS"
type="javax.sql.DataSource"
maxIdle="2"
maxTotal="20"
maxWaitMillis="5000"
driverClassName="org.postgresql.Driver"
username="cuba"
password="cuba"
url="jdbc:postgresql://localhost/sales"/>
Microsoft SQL Server 2005 数据源定义示例:
<Resource
name="jdbc/CubaDS"
type="javax.sql.DataSource"
maxIdle="2"
maxTotal="20"
maxWaitMillis="5000"
driverClassName="net.sourceforge.jtds.jdbc.Driver"
username="sa"
password="saPass1"
url="jdbc:jtds:sqlserver://localhost/sales"/>
Microsoft SQL Server 2008+ 数据源定义示例:
<Resource
name="jdbc/CubaDS"
type="javax.sql.DataSource"
maxIdle="2"
maxTotal="20"
maxWaitMillis="5000"
driverClassName="com.microsoft.sqlserver.jdbc.SQLServerDriver"
username="sa"
password="saPass1"
url="jdbc:sqlserver://localhost;databaseName=sales"/>
Oracle 数据源定义示例:
<Resource
name="jdbc/CubaDS"
type="javax.sql.DataSource"
maxIdle="2"
maxTotal="20"
maxWaitMillis="5000"
driverClassName="oracle.jdbc.OracleDriver"
username="sales"
password="sales"
url="jdbc:oracle:thin:@//localhost:1521/orcl"/>
MySQL 数据源定义示例:
<Resource
type="javax.sql.DataSource"
name="jdbc/CubaDS"
maxIdle="2"
maxTotal="20"
maxWaitMillis="5000"
driverClassName="com.mysql.jdbc.Driver"
password="cuba"
username="cuba"
url="jdbc:mysql://localhost/sales?useSSL=false&allowMultiQueries=true"/>
下面这一行禁用了 HTTP 会话的序列化:
<Manager pathname=""/>
A.3. default-permission-values.xml
这种类型的文件用来定义用户的默认权限值。如果没有角色为需要权限的目标显式定义权限值的时候,就会使用默认权限值。对于有拒绝访问权限的用户来说,这个文件很多时候是需要的:因为如果没有这个文件,有拒绝访问角色的用户默认情况下是连主窗口界面和过滤器界面都不能访问的。
这种类型的文件是不能通过 Studio 自动生成的,需要在 core 模块手动创建。
文件的地址通过 [cuba.defaultPermissionValuesConfig] 应用程序属性定义。如果应用程序中这个属性没有定义,则会使用默认的 cuba-default-permission-values.xml
文件。
这个文件有如下结构:
default-permission-values
- 根元素,只有一个嵌套元素 - permission
。
permission
- 权限许可:定义对象类型和针对这个对象类型的权限。
permission
有三个属性:
-
target
- 许可对象:定义权限应用的特殊对象。根据许可类型来定义这个属性的格式:对于界面 - 界面的id
,对于实体操作 - 实体的id
和操作类型,比如,target="sec$Filter:read"
,等等。 -
value
- 许可值。可以是0
或者1
,分别表示拒绝或者许可。 -
type
- 权限许可对象的类型:-
10
- screen - 界面, -
20
- entity operation - 实体操作, -
30
- entity attribute - 实体属性, -
40
- application-specific permission - 程序特定功能权限, -
50
- UI component - 界面组件.
-
示例:
<?xml version="1.0" encoding="UTF-8"?>
<default-permission-values xmlns="http://schemas.haulmont.com/cuba/default-permission-values.xsd">
<permission target="dynamicAttributesConditionEditor" value="0" type="10"/>
<permission target="dynamicAttributesConditionFrame" value="0" type="10"/>
<permission target="sec$Filter:read" value="1" type="20"/>
<permission target="cuba.gui.loginToClient" value="1" type="40"/>
</default-permission-values>
A.4. dispatcher-spring.xml
这种类型的文件为包含 Spring MVC 控制器(controller)的客户端 blocks 定义了一个额外的 Spring 框架容器的配置。
这个给控制器创建的额外 Spring 容器是使用 Spring 主容器(配置在 spring.xml 文件)作为父容器来创建的。因此,在这个控制器容器内的 bean 可以使用主容器的 bean,但是主容器的 bean 却“看不见”控制器容器内的 bean。
项目的 dispatcher-spring.xml
文件是通过 cuba.dispatcherSpringContextConfig 应用程序属性来定义的。
平台的 web 和 portal 模块已经分别包含了这个配置文件:cuba-dispatcher-spring.xml
和 cuba-portal-dispatcher-spring.xml
如果在项目中创建了 Spring MVC 控制器(比如在 web 模块),需要添加如下配置:
-
假设控制器都在
com.company.sample.web.controller
包内创建,创建modules/web/src/com/company/sample/web/dispatcher-config.xml
文件并且添加如下内容:<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd"> <context:annotation-config/> <context:component-scan base-package="com.company.sample.web.controller"/> </beans>
-
在
web-app.properties
文件的 cuba.dispatcherSpringContextConfig 属性添加这个配置文件:cuba.dispatcherSpringContextConfig = +com/company/sample/web/dispatcher-config.xml
web 模块中定义的控制器可以通过 dispatcher
servlet 的 URL 地址访问,默认是以 /dispatch
开头。示例:
http://localhost:8080/app/dispatch/my-controller-endpoint
portal 模块中定义的控制器可以通过 web 应用程序的根节点访问,比如:
http://localhost:8080/app-portal/my-controller-endpoint
A.5. menu.xml
这个类型的文件在 Web 客户端使用,定义应用程序的主菜单结构。
XML schema: http://schemas.haulmont.com/cuba/7.0/menu.xsd
这个文件的路径由 cuba.menuConfig 应用程序属性指定。当在 Studio 里面创建新项目的时候,会在 web 模块包的根目录创建 web-menu.xml
文件,比如 modules/web/src/com/company/sample/web-menu.xml
。
menu-config
– XML 根节点元素。menu-config
的元素组成了一个树状结构,这里 menu
元素是树枝,item
和 separator
元素是树叶。
menu
元素的属性:
item
元素的属性:
-
id
– 元素的唯一标识符,菜单元素的本地化标题通过 id 来决定(参考如下)。如果没有定义任何screen
,bean
,class
属性,此 id 用来指向具有相同 id 值的界面。当用户点击菜单项时,会在程序主窗口打开对应的界面。<item id="sample_Foo.browse"/>
-
screen
- 界面标识符。可以用这个标识符在菜单中多次包含同一界面。当用户点击菜单项时,会在程序主窗口打开对应的界面。<item id="foo1" screen="sample_Foo.browse"/> <item id="foo2" screen="sample_Foo.browse"/>
-
bean
- bean 名称。必须跟beanMethod
一起使用。当用户点击菜单项时,会调用 bean 的此方法。<item bean="sample_FooProcessor" beanMethod="processFoo"/>
-
class
- 实现了Runnable
的类全名。当用户点击菜单项时,会创建指定类的一个对象,并调用其run()
方法。<item class="com.company.sample.web.FooProcessor"/>
-
description
- 光标悬浮提示窗显示的文字。可以从主语言包中使用本地化语言。<item id="sample_Foo.browse" description="mainMsg://fooBrowseDescription"/>
-
shortcut
– 此菜单项的快捷键。可以用ALT
,CTRL
,SHIFT
功能键,用-
分隔,比如:shortcut="ALT-C" shortcut="ALT-CTRL-C" shortcut="ALT-CTRL-SHIFT-C"
快捷键也可以通过应用程序属性来配置,然后在
menu.xml
文件中通过下列方式来使用:shortcut="${sales.menu.customer}"
-
openType
– 界面打开模式。对应于OpenMode
枚举:NEW_TAB
,THIS_TAB
,DIALOG
。默认值是NEW_TAB
。 -
icon
- 菜单元素的图标。参考 icon 了解细节。 -
insertBefore
,insertAfter
– 设定此项应当在一个特定元素或者标识符指定的特定菜单项之前或者之后。 -
resizable
– 只跟DIALOG
界面打开模式有关。控制界面是否能改变大小。可选值:true
,false
。默认情况下主菜单不会影响弹出窗口的改变大小的功能。 -
stylename
- 为菜单项定义一个样式名称。参考 主题 了解细节。-
item
的子元素:
-
-
param
– 传递到界面旧控制器的init()
方法的参数。在menu.xml
里配置的参数会覆盖在 screens.xml 里配置的同名参数。param
的属性:-
name
– 参数名称 -
value
– 参数值。字符串类型,按照下面的规则可以转成其它类型的对象:-
如果字符串的值是一个实体的标识符,按照
EntityLoadInfo
类规定的格式,系统会加载指定的实体实例。 -
如果字符串是
${some_name}
格式的,参数的值会被设置为some_name
应用程序属性的值。 -
true
和false
的字符串会被转化为Boolean
类型的相应值。 -
其它情况,用字符串本身的值作为参数值。
-
-
-
permissions
– 设置一组权限,用户只有满足这些权限才能使用此菜单项。这个机制应当在菜单项需要绑定到特定的权限或者绑定到多个权限时使用。大多数情况下,安全子系统的标准功能应该足以满足根据界面标识符控制菜单可用性的要求。这个元素应当包含一组
permission
子元素,每个元素描述一条需要的权限。菜单项只有在所有权限都满足的情况下才可用。permission
属性:-
type
– 权限类型。可用这些PermissionType
:SCREEN
,ENTITY_OP
,ENTITY_ATTR
,SPECIFIC
,UI
。 -
target
– 用来检查权限的对象(object)。根据权限类型会不同:-
SCREEN
– 界面标识符,比如sales$Customer.lookup
。 -
ENTITY_OP
– 使用{entity_name}:{op}
格式的字符串,这里{op}
可以是:read
,create
,update
,delete
。比如sales$Customer:create
。 -
ENTITY_ATTR
– 使用{entity_name}:{attribute}
格式的字符串,比如sales$Customer:name
。 -
SPECIFIC
– 特定的权限标识符,比如sales.runInvoicing
。 -
UI
– 界面可视化组件的路径。
-
-
菜单文件的示例:
<menu-config xmlns="http://schemas.haulmont.com/cuba/menu.xsd">
<menu id="sales" insertBefore="administration">
<item id="sales_Customer.lookup"/>
<separator/>
<item id="sales_Order.lookup"/>
</menu>
</menu-config>
menu-config.sales=Sales
menu-config.sales$Customer.lookup=Customers
如果没设置 id
,菜单元素的名称会通过类名(如果设置了 class
属性)或者 bean 名称和 bean 方法名称(如果设置了 bean
属性)生成,因此,推荐设置 id
属性。
A.6. metadata.xml
这种类型的文件用来注册自定义的数据类型以及非持久化实体并且设置元注解(meta-annotations)。
XML schema 标准: http://schemas.haulmont.com/cuba/7.0/metadata.xsd。
项目的 metadata.xml
文件通过 cuba.metadataConfig 应用程序属性来指定。
文件有如下结构:
metadata
– 根元素。
metadata
的元素:
-
datatypes
- 自定义类型的一个可选描述。datatypes
的元素:-
datatype
- 数据类型描述,有如下属性:-
id
- 标识符,用来在 @MetaProperty 注解中表示这个数据类型。 -
class
- 定义实现类 -
sqlType
- 可选参数,用来保存此数据类型值的数据库 SQL 类型(数据库字段类型)。CUBA Studio 会在生成数据库脚本的时候使用这个 SQL 类型。参考 自定义数据类型示例 了解细节。
datatype
元素可以包含其它依赖这个数据类型实现的属性。 -
-
-
metadata-model
– 项目元数据模型描述符。metadata-model
的属性:-
root-package
– 项目包的根目录。
metadata-model
的元素:-
class
– 非持久化实体类
-
-
annotations
– 包含实体元注解的设置。annotations
元素包含entity
元素定义元注解设置的实体类。每个entity
元素必须包含class
属性来指定实体类,以及一组annotation
元素。annotation
元素用来定义元注解,用name
属性来指定元注解的名称。元注解的其它属性通过一组attribute
子元素来指定。
示例:
<metadata xmlns="http://schemas.haulmont.com/cuba/metadata.xsd">
<metadata-model root-package="com.sample.sales">
<class>com.sample.sales.entity.SomeNonPersistentEntity</class>
<class>com.sample.sales.entity.OtherNonPersistentEntity</class>
</metadata-model>
<annotations>
<entity class="com.haulmont.cuba.security.entity.User">
<annotation name="com.haulmont.cuba.core.entity.annotation.TrackEditScreenHistory">
<attribute name="value" value="true" datatype="boolean"/>
</annotation>
<annotation name="com.haulmont.cuba.core.entity.annotation.EnableRestore">
<attribute name="value" value="true" datatype="boolean"/>
</annotation>
</entity>
<entity class="com.haulmont.cuba.core.entity.Category">
<annotation name="com.haulmont.cuba.core.entity.annotation.SystemLevel">
<attribute name="value" value="false" datatype="boolean"/>
</annotation>
</entity>
</annotations>
</metadata>
A.7. permissions.xml
这个类型的文件用在 Web 客户端 block,用来注册特殊的用户权限。
文件的路径通过 cuba.permissionConfig 应用程序属性指定。当通过 Studio 创建新项目的时候,会在 web 模块包的根目录创建 web-permissions.xml
文件,比如 modules/web/src/com/company/sample/web-permissions.xml
。
这个文件有如下结构:
permission-config
- 根节点元素。
permission-config
的元素:
-
specific
- 特殊权限描述符。specific
的元素:
示例:
<permission-config xmlns="http://schemas.haulmont.com/cuba/permissions.xsd">
<specific>
<category id="app">
<permission id="app.doSomething"/>
<permission id="app.doSomethingOther"/>
</category>
</specific>
</permission-config>
A.8. persistence.xml
这种类型的文件是 JPA 的标准文件,用来注册持久化实体以及 ORM 框架参数配置。
项目的 persistence.xml
文件通过应用程序属性 cuba.persistenceConfig 定义。
当 Middleware block 启动时,这些文件会被组合成单一的 persistence.xml
文件,保存在应用程序的 work folder 目录。文件的顺序很重要,因为列表中后面的文件会覆盖前面文件的 ORM 参数设置。
一个文件示例:
<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="1.0">
<persistence-unit name="sales" transaction-type="RESOURCE_LOCAL">
<class>com.sample.sales.entity.Customer</class>
<class>com.sample.sales.entity.Order</class>
</persistence-unit>
</persistence>
A.9. remoting-spring.xml
这种类型的文件为 Middleware block 配置了一个额外的 Spring Framework 容器,用来暴露 service 和其它中间件组件,以便客户端层访问(从这里开始称之为 remote access container - 远程访问容器)。
项目的 remoting-spring.xml
文件通过 cuba.remotingSpringContextConfig 应用程序属性指定。
远程访问容器使用 Spring 主容器(在 spring.xml 文件配置主容器)作为父容器进行创建。因此,远程访问容器里的 bean 可以使用主容器内的 bean,但是主容器内的 bean 却“看不见”远程访问容器内的 bean。
远程访问的主要目的是使 Middleware 的服务能从客户端级别通过 Spring HttpInvoker 机制访问。在 cuba 应用程序组件中的 cuba-remoting-spring.xml
文件定义了 RemoteServicesBeanCreator
类型的 servicesExporter
bean,这个 bean 从主容器获得所有的 service 类,然后 export 他们。作为通常带注解的 service 的补充,远程访问容器 export 了一批特殊的 bean,比如 LoginService
。
还有,cuba-remoting-spring.xml
文件定义了一个基础包用来作为查找带有注解的 Spring MVC 下载和上传控制器类的入口。
项目中的 remoting-spring.xml
文件只有在使用了特殊的 Spring MVC 控制器的时候才需要创建。项目中的 services 会通过定义在 cuba 应用程序组件中标准的 servicesExporter
bean 来引入。
A.10. spring.xml
这个类型的文件为每个应用程序 block 配置 Spring Framework 的主容器。
项目的 spring.xml
文件路径通过 cuba.springContextConfig 应用程序属性指定。
大多数主容器的配置都通过 bean 的注解完成(比如 @Component
, @Service
, @Inject
等等),因此项目中 spring.xml 的唯一必须部分就是定义 context:component-scan
元素,用来指定查找注解类的基本 Java 包名。示例:
<context:component-scan base-package="com.sample.sales"/>
A.11. views.xml
这个类型的文件用来描述视图(views),参考 视图。
XML schema: http://schemas.haulmont.com/cuba/7.0/view.xsd.
views
– 根节点元素。
views
的元素:
-
view
–view
视图描述元素。view
属性:-
class
– 实体类。 -
entity
– 实体名称,比如sales$Order
。这个属性可以用来替代class
属性。 -
name
– 视图名称,实体范围内需要名称唯一。 -
systemProperties
– 启用包含定义在持久化实体BaseEntity
基类和Updatable
接口中的基础接口系统属性。此参数为可选参数,默认为true
。 -
overwrite
– 启用覆盖视图功能,通过同一类以及部署在仓库(repository)的名称来覆盖同名视图。可选参数,默认为false
。 -
extends
– 指定一个用来继承实体属性的实体视图。比如,声明extends="_local"
,这样会将实体的所有 local attributes 添加到当前视图。也是可选参数。
view
的元素:-
property
–ViewProperty
视图属性描述元素。
property
的属性:-
name
– 实体属性名称。 -
view
– 对于引用类型属性,设定一个实体关联的视图名称,用来加载实体的属性。 -
fetch
- 对于引用类型属性,设定如何从数据库取关联实体。参考 视图 了解细节。
property
的元素:-
property
– 跟实体属性描述关联。这个用来在当前描述中定义一个关联实体的无命名单行(inline)视图。
-
-
include
– 包含另外一个views.xml
文件。include
的属性:-
file
– 文件路径,符合 Resources 接口规范。
-
示例:
<views xmlns="http://schemas.haulmont.com/cuba/view.xsd">
<view class="com.sample.sales.entity.Order"
name="order-with-customer"
extends="_local">
<property name="customer" view="_minimal"/>
</view>
<view class="com.sample.sales.entity.Item"
name="itemsInOrder">
<property name="quantity"/>
<property name="product" view="_minimal"/>
</view>
<view class="com.sample.sales.entity.Order"
name="order-with-customer-defined-inline"
extends="_local">
<property name="customer">
<property name="name"/>
<property name="email"/>
</property>
</view>
</views>
也可以参考 cuba.viewsConfig 应用程序属性。
A.12. web.xml
web.xml
是 Java EE web 应用程序的标准描述文件。需要为 Middleware、web 客户端以及 Web Portal 客户端 block 创建此文件。
在一个应用程序项目中,web.xml
文件在相应模块的 web/WEB-INF
目录。
-
Middleware block(core 项目模块)的
web.xml
文件有如下内容:<?xml version="1.0" encoding="UTF-8" standalone="no"?> <web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0"> <!-- Application properties config files --> <context-param> <param-name>appPropertiesConfig</param-name> <param-value> classpath:com/company/sample/app.properties /WEB-INF/local.app.properties "file:${catalina.base}/conf/app-core/local.app.properties" </param-value> </context-param> <!--Application components--> <context-param> <param-name>appComponents</param-name> <param-value>com.haulmont.cuba com.haulmont.reports</param-value> </context-param> <listener> <listener-class>com.haulmont.cuba.core.sys.AppContextLoader</listener-class> </listener> <servlet> <servlet-name>remoting</servlet-name> <servlet-class>com.haulmont.cuba.core.sys.remoting.RemotingServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>remoting</servlet-name> <url-pattern>/remoting/*</url-pattern> </servlet-mapping> </web-app>
context-param
元素定义了当前 web 应用程序ServletContext
对象的初始化参数。应用程序组件列表定义在appComponents
参数,应用程序属性文件列表定义在appPropertiesConfig
参数。listener
元素定义了实现ServletContextListener
接口的监听类。Middleware block 使用AppContextLoader
类作为监听器。这个类初始化了 AppContext。然后是 Servlet 描述,包括
RemotingServlet
类,对于 Middleware block 来说,这是必须的。这个 servlet 可以通过/remoting/*
URL 来访问,跟远程访问容器相关联,参考 remoting-spring.xml。 -
Web 客户端 block(web 项目模块)的
web.xml
文件有如下内容:<?xml version="1.0" encoding="UTF-8" standalone="no"?> <web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0"> <context-param> <description>Web resources version for correct caching in browser</description> <param-name>webResourcesTs</param-name> <param-value>${webResourcesTs}</param-value> </context-param> <!-- Application properties config files --> <context-param> <param-name>appPropertiesConfig</param-name> <param-value> classpath:com/company/sample/web-app.properties /WEB-INF/local.app.properties "file:${catalina.base}/conf/app/local.app.properties" </param-value> </context-param> <!--Application components--> <context-param> <param-name>appComponents</param-name> <param-value>com.haulmont.cuba com.haulmont.reports</param-value> </context-param> <listener> <listener-class>com.vaadin.server.communication.JSR356WebsocketInitializer</listener-class> </listener> <listener> <listener-class>com.haulmont.cuba.web.sys.WebAppContextLoader</listener-class> </listener> <servlet> <servlet-name>app_servlet</servlet-name> <servlet-class>com.haulmont.cuba.web.sys.CubaApplicationServlet</servlet-class> <async-supported>true</async-supported> </servlet> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>com.haulmont.cuba.web.sys.CubaDispatcherServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet> <servlet-name>rest_api</servlet-name> <servlet-class>com.haulmont.restapi.sys.CubaRestApiServlet</servlet-class> <load-on-startup>2</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/dispatch/*</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>app_servlet</servlet-name> <url-pattern>/*</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>rest_api</servlet-name> <url-pattern>/rest/*</url-pattern> </servlet-mapping> <filter> <filter-name>cuba_filter</filter-name> <filter-class>com.haulmont.cuba.web.sys.CubaHttpFilter</filter-class> <async-supported>true</async-supported> </filter> <filter-mapping> <filter-name>cuba_filter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter> <filter-name>restSpringSecurityFilterChain</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <init-param> <param-name>contextAttribute</param-name> <param-value>org.springframework.web.servlet.FrameworkServlet.CONTEXT.rest_api</param-value> </init-param> <init-param> <param-name>targetBeanName</param-name> <param-value>springSecurityFilterChain</param-value> </init-param> </filter> <filter-mapping> <filter-name>restSpringSecurityFilterChain</filter-name> <url-pattern>/rest/*</url-pattern> </filter-mapping> </web-app>
Web 客户端 block 使用
WebAppContextLoader
类作为ServletContextListener
。JSR356WebsocketInitializer
是支持 WebSockets 协议需要的监听器。CubaApplicationServlet
提供了基于 Vaadin 框架实现的通用用户界面。CubaDispatcherServlet
为 Spring MCV 控制器初始化了一个额外的 Spring context。这个 context 通过 dispatcher-spring.xml 文件来配置。CubaRestApiServlet
提供全局的 REST API。
Appendix B: 应用程序属性
本附录章节按字母顺序介绍所有可用的应用程序属性。
- cuba.additionalStores
- cuba.allowQueryFromSelected
-
启用通用过滤器的级联过滤模式。参考 级联查询(Sequential Queries)。
默认值:
true
保存在数据库。
配置接口:
GlobalConfig
可以在 Web 客户端和 Middleware 这两个 block 使用。
- cuba.anonymousLogin
-
用来创建匿名用户会话的用户名称。(参考 cuba.anonymousSessionId)。
默认值:
anonymous
保存在数据库。
配置接口:
ServerConfig
可以在 Middleware block 使用。
- cuba.anonymousSessionId
-
定义匿名用户会话的 UUID,此会话可以在没有用户登录的状态下使用。在服务启动的时候,会自动创建这个匿名会话。参考 cuba.anonymousLogin。
配置接口:
GlobalConfig
在所有标准的 blocks 中都可以使用。
- cuba.automaticDatabaseUpdate
-
- cuba.availableLocales
-
支持的用户界面语言列表。
属性格式:
{language_name1}|{language_code_1};{language_name2}|{language_code_2};
…示例:
cuba.availableLocales=French|fr;English|en
{language_name}
– 显示在可用语言列表中的语言名称。比如这个列表会被用在登录界面提供给用户选择系统语言,也在用户编辑界面,编辑用户的语言。{language_code}
– 对应于Locale.getLanguage()
方法返回的语言代码。用来作为语言包文件名称的后缀,比如,messages_fr.properties
。如果语言列表中没有跟用户操作系统语言相匹配的条目,那么
cuba.availableLocales
属性定义的语言列表的第一个语言将被用来作为默认语言。否则,即会选择跟用户操作系统语言相匹配的做为默认语言。默认值:
English|en;Russian|ru;French|fr
配置接口:
GlobalConfig
在所有标准的 blocks 中都可以使用。
- cuba.backgroundWorker.maxActiveTasksCount
-
- cuba.backgroundWorker.timeoutCheckInterval
-
- cuba.bruteForceProtection.enabled
-
启用针对野蛮破解密码的保护措施。
默认值:
false
保存在数据库。
配置接口:
ServerConfig
可以在 Middleware block 使用。
- cuba.bruteForceProtection.blockIntervalSec
-
超过最大失败登录尝试次数之后屏蔽登录的时间间隔,单位是秒,需要先启用 cuba.bruteForceProtection.enabled。
默认值: 60
保存在数据库。
配置接口:
ServerConfig
可以在 Middleware block 使用。
- cuba.bruteForceProtection.maxLoginAttemptsNumber
-
针对用户名和登录 IP 设定的最大失败登录尝试次数,需要先启用 cuba.bruteForceProtection.enabled。
默认值: 5
保存在数据库。
配置接口:
ServerConfig
可以在 Middleware block 使用。
- cuba.checkPasswordOnClient
-
当设置为 false(默认值)时,客户端块 block 的
LoginPasswordLoginProvider
将用户密码明文发送给中间件的AuthenticationService.login()
方法。在客户端和中间件 block 共同位于同一 JVM 中的情况下,这是合适的处理方式。对于客户端块(block)位于网络上的其它计算机上的分布式部署的情况,客户端和中间件之间的连接应使用 SSL 加密。如果设置为 true,
LoginPasswordLoginProvider`本身将通过输入的登录名加载 `User
实体并检查密码。如果密码与加载的密码哈希匹配,则提供程序使用cuba.trustedClientPassword 属性中指定的密码作为可信客户端执行登录。此模式使您无需在受信任网络中的客户端和中间件之间设置 SSL 连接,同时不会向网络公开用户密码:仅传输哈希值。但请注意,可信客户端密码仍然通过网络传输,因此受 SSL 保护的连接仍然更加安全。默认值 :
false
接口:
WebAuthConfig
,PortalConfig
,RestApiConfig
可在 Web 和 Porta block 使用。
- cuba.cluster.enabled
-
- cuba.cluster.jgroupsConfig
-
JGroups 配置文件的路径。平台使用 Resource - 资源接口来加载此文件,所以这个文件可以放在 classpath 或者配置文件目录。
示例:
cuba.cluster.jgroupsConfig = my_jgroups_tcp.xml
默认值:
jgroups.xml
可以在 Middleware block 使用。
- cuba.cluster.messageSendingQueueCapacity
-
限制 Middleware 集群中消息队列的长度。当消息队列超过了最大长度,新消息会被拒绝。
默认值:
Integer.MAX_VALUE
可以在 Middleware block 使用。
- cuba.cluster.stateTransferTimeout
-
设置节点启动时从集群接收最新状态的超时时间。单位是毫秒。
默认值:
10000
可以在 Middleware block 使用。
- cuba.confDir
-
对于 Tomcat 快速部署,默认值:
${catalina.home}/conf/${cuba.webContextName}
,指向tomcat/conf
目录下 web app 名称的目录,比如tomcat/conf/app-core
。对于 WAR 和 UberJAR 部署情况:
${app.home}/${cuba.webContextName}/conf
,指向应用程序根目录下的一个目录。配置接口:
GlobalConfig
在所有标准的 blocks 中都可以使用。
- cuba.connectionReadTimeout
-
在客户端 block 设置连接 Middleware 读取超时的时限。平台会将非负值传递给
URLConnection
的setReadTimeout()
方法。也可参考 cuba.connectionTimeout。
默认值:
-1
可以在 Web 客户端,Web Portal blocks 使用。
- cuba.connectionTimeout
-
在客户端 block 设置连接 Middleware 超时的时限。平台会将非负值传递给
URLConnection
的setConnectTimeout()
方法。默认值:
-1
可以在 Web 客户端,Web Portal blcoks 使用。
- cuba.connectionUrlList
-
为客户端 blocks 设置连接 Middleware 服务的 URL。
此属性的值应该包含多个用英文逗号分隔 URL
http[s]://host[:port]/app-core
,host
是服务器名称,port
是服务器端口,app-core
是 Middleware web app 的名称。比如:cuba.connectionUrlList = http://localhost:8080/app-core
当使用 Middleware 服务集群的时候,这些服务的地址需要用英文逗号分隔:
cuba.connectionUrlList = http://server1:8080/app-core,http://server2:8080/app-core
细节参考 配置连接 Middleware 集群。
配置接口:
ClientConfig
可以在 Web 客户端,Web Portal blcoks 使用。
- cuba.creditsConfig
-
平台使用 Resource - 资源接口来加载此文件,所以这个文件可以放在 classpath 或者配置文件目录。
可以在 Web 客户端 block 使用。
示例:
cuba.creditsConfig = +com/company/base/credits.xml
- cuba.crossDataStoreReferenceLoadingBatchSize
-
- cuba.dataManagerChecksSecurityOnMiddleware
-
配置在 Middleware 调用 DataManager 时是否启用安全限制。
默认值:
false
保存在数据库。
配置接口:
ServerConfig
可以在 Middleware block 使用。
- cuba.dataSourceJndiName
-
定义应用数据库连接中使用的
javax.sql.DataSource
的 JNDI 名称。默认值:
java:comp/env/jdbc/CubaDS
可以在 Middleware block 使用。
- cuba.dataDir
-
对于 Tomcat 快速部署,默认值:
${catalina.home}/work/${cuba.webContextName}
,指向tomcat/work
目录下 web app 名称的目录,比如tomcat/work/app-core
。对于 WAR 和 UberJAR 部署情况:
${app.home}/${cuba.webContextName}/work
,指向应用程序根目录下的一个目录。配置接口:
GlobalConfig
在所有标准的 blocks 中都可以使用。
- cuba.dbDir
-
设置数据库脚本目录的位置。
对于 Tomcat 快速部署,默认值:
${catalina.home}/webapps/${cuba.webContextName}/WEB-INF/db
,指向 Tomcat 中 web app 的WEB-INF/db
子目录。对于 WAR 和 UberJAR 部署情况:
web-inf:db
,指向 WAR 或者 UberJAR 内的WEB-INF/db
目录。配置接口:
ServerConfig
可以在 Middleware block 使用。
- cuba.dbmsType
-
定义 DBMS 类型。跟 cuba.dbmsVersion 一起作用,影响对于 DBMS 集成接口实现的选取,以及查找数据库初始化和更新脚本。
参考 DBMS 类型 了解细节。
默认值:
hsql
可以在 Middleware block 使用。
- cuba.dbmsVersion
-
可选属性,设置数据库版本。跟 cuba.dbmsType 一起作用,影响对于 DBMS 集成接口实现的选取,以及查找数据库初始化和更新脚本。
参考 DBMS 类型 了解细节。
默认值:
none
可以在 Middleware block 使用。
- cuba.defaultPermissionValuesConfig
-
定义包含用户默认权限的一组文件。当没有为许可对象设置角色的时候,会使用默认权限值。通常用来为“拒绝”角色使用,参考 default-permission-values.xml 章节了解更多信息。
默认值:
cuba-default-permission-values.xml
可以在 Middleware block 使用。
示例:
cuba.defaultPermissionValuesConfig = +my-default-permission-values.xml
- cuba.defaultQueryTimeoutSec
-
- cuba.disableEscapingLikeForDataStores
- cuba.disableOrmXmlGeneration
-
- cuba.dispatcherSpringContextConfig
-
累加属性,为客户端 block 定义 dispatcher-spring.xml 文件。
平台使用 Resource - 资源接口来加载此文件,所以这个文件可以放在 classpath 或者配置文件目录。
可以在 Web 客户端和 Web Portal blocks 使用。
示例:
cuba.dispatcherSpringContextConfig = +com/company/sample/portal-dispatcher-spring.xml
- cuba.download.directories
- cuba.email.*
-
在 配置电子邮件发送参数 有关于发送邮件参数的介绍。
- cuba.fileStorageDir
-
- cuba.enableDeleteStatementInSoftDeleteMode
-
向后兼容的开关。如果设置为
true
,在软删除模式开启的情况下,开启为软删除的实体执行delete from
的 JPQL 语句(软删除开启的情况,对实体只是运行update
而非delete
)。这样的 delete 语句会被转换成删除所有没有标记为“已删除”的数据。这样的话,有点违反直观理解,所以默认情况下是关闭此功能的。默认值:
false
可以在 Middleware block 使用。
- cuba.enableSessionParamsInQueryFilter
- cuba.entityAttributePermissionChecking
-
如果设置为
true
,在 Middleware 开启实体属性权限检查。如果设置为false
,属性权限检查则在客户端层做,比如在 Generic UI 或者 REST API。默认值:
false
保存在数据库。
可以在 Middleware block 使用。
- cuba.entityLog.enabled
-
- cuba.groovyEvaluationPoolMaxIdle
- cuba.groovyEvaluatorImport
- cuba.gui.genericFilterChecking
-
- cuba.gui.genericFilterColumnsCount
-
- cuba.gui.genericFilterConditionsLocation
-
定义在过滤器组件中条件面板的位置。两种位置可以选择:
top
(在过滤器控制器元素之上)和bottom
(在过滤器控制器元素之下)。默认值:
top
保存在数据库。
配置接口:
ClientConfig
可以在 Web 客户端 block 使用。
- cuba.gui.genericFilterControlsLayout
-
为过滤器组件的控制器布局设置模板。每个控制器有这样的格式:
[component_name | options-comma-separated]
,比如,[pin | no-caption, no-icon]
。可用的控制器:
-
filters_popup
- 选择过滤器的弹窗按钮,整合了 Search 按钮功能。 -
filters_lookup
- 选择过滤器的查找字段。需要单独添加 Search 按钮。 -
search
- Search 按钮。如果使用filters_popup
则不需要添加。 -
add_condition
- 添加新条件的按钮。 -
spacer
- 控制器之间空白的分隔符。 -
settings
- Settings 按钮。设置在 Settings 弹窗中显示的 action 选项的名称。(参考下面)。 -
max_results
- 控制器组,用来设置选择记录的最大值。 -
fts_switch
- 用来切换到全文检索(Full-Text Search)模式的复选框。
以下这些操作可以在
settings
中作为选项使用:save - 保存
,save_as - 另存为
,edit - 编辑
,remove - 删除
,pin - 固定位置
,make_default - 设置默认
,save_search_folder - 保存搜索目录
,save_app_folder - 保存应用目录
,clear_values - 清空
。这些操作也可以在 Settings 弹窗外作为单独的控制器使用。这种情况下,它们可以做如下设置:
-
no-icon
- 设置动作按钮是否不带图标,示例:[save | no-icon]
。 -
no-caption
- 设置动作按钮是否不带名称,示例:[pin | no-caption]
。
默认值:
[filters_popup] [add_condition] [spacer] \ [settings | save, save_as, edit, remove, make_default, pin, save_search_folder, save_app_folder, clear_values] \ [max_results] [fts_switch]
保存在数据库。
配置接口:
ClientConfig
可以在 Web 客户端 block 使用。
-
- cuba.gui.genericFilterManualApplyRequired
-
影响过滤器组件的行为。
当设置为
true
,包含过滤器的界面不会自动加载相应的数据源,需要用户手动点击过滤器的 Apply 按钮。当使用应用或者查找目录打开界面的时候,
cuba.gui.genericFilterManualApplyRequired
的设置会被忽略,因为此时过滤器已经生效了。但是对于某个查找目录如果它的applyDefault
设置为false
,过滤器不会生效。默认值:
false
保存在数据库。
配置接口:
ClientConfig
可以在 Web 客户端 block 使用。
- cuba.gui.genericFilterMaxResultsOptions
-
定义过滤器组件中 Show rows 下拉框中的选项值。
NULL 选项表示这个列表需要包含一个空值。
默认值:
NULL, 20, 50, 100, 500, 1000, 5000
保存在数据库。
配置接口:
ClientConfig
可以在 Web 客户端 block 使用。
- cuba.gui.genericFilterPopupListSize
-
定义在 Search 按钮的弹窗列表中显示项目的数量。如果过滤器的数量超过了这个值,则会添加
Show more…
到列表最后,这个行为会打开一个新的弹窗用来显示其它的过滤器。默认值:
10
保存在数据库。
配置接口:
ClientConfig
可以在 Web 客户端 block 使用。
- cuba.gui.genericFilterTrimParamValues
-
定义所有的通用过滤器是否需要去掉输入值两端的空格。当设置为
false
,文本过滤器将保留空格。默认值:
true
保存在数据库。
配置接口:
ClientConfig
可以在 Web 客户端 block 使用。
- cuba.gui.layoutAnalyzerEnabled
-
可以禁用主窗口标签页以及模式窗口标题的右键菜单中的界面分析器。
默认值:
true
保存在数据库。
配置接口:
ClientConfig
可以在 Web 客户端 block 使用。
- cuba.gui.lookupFieldPageLength
-
定义在下拉框控件和下拉框选择器控件中下拉列表一页显示的选项默认数量。可以通过 XML 属性 pageLength 在具体实例中覆盖这个参数的设置。
默认值: 10
保存在数据库。
配置接口:
ClientConfig
可以在 Web 客户端使用。
- cuba.gui.manualScreenSettingsSaving
-
如果此属性设置为
true
,界面不会在关闭时自动保存界面设置。在这个模式下,用户可以通过右键点击界面 tab 标签或者弹窗的标题类保存或者重置界面设置。默认值:
false
配置接口:
ClientConfig
保存在数据库。
可以在 Web 客户端 block 使用。
- cuba.gui.showIconsForPopupMenuActions
-
- cuba.gui.systemInfoScriptsEnabled
-
启用在 System Information 窗口创建/更新/获取实体实例的时候显示 SQL 脚本。
这些脚本实际上显示的选中实体实例的数据库行内容,而不管是否有安全设置可能禁止显示某些实体属性。所以这就是为什么除了 administrator 用户之外需要取消其它所有用户角色的 CUBA / Generic UI / System Information 特殊权限。也可以通过设置
cuba.gui.systemInfoScriptsEnabled
为false
在整个应用级别禁止这个功能。默认值:
true
保存在数据库。
配置接口:
ClientConfig
可以在 Web 客户端 block 使用。
- cuba.gui.useSaveConfirmation
-
当用户关闭带有未保存改动数据源的界面时,定义对话框的布局样式。
true
对应带有三个功能的布局:Save changes - 保存修改, Don’t Save - 不保存修改, Don’t close the screen - 不关窗口。false
对应带有两个功能的布局:Close the screen without saving changes - 关闭窗口不保存修改, Don’t close the screen - 不关窗口。默认值:
true
保存在数据库。
配置接口:
ClientConfig
可以在 Web 客户端 block 使用。
- cuba.gui.validationNotificationType
-
定义标准的界面验证错误的通知消息类型。
可选值是
com.haulmont.cuba.gui.components.Frame.NotificationType
枚举类型的元素:-
TRAY
- 右下角的通知消息,带有普通消息文本。 -
TRAY_HTML
- 右下角的通知消息,带有 HTML 消息文本。 -
HUMANIZED
- 标准通知消息,显示在界面中间,带有普通消息文本。 -
HUMANIZED_HTML
- 标准通知消息,显示在界面中间,带有 HTML 消息文本。 -
WARNING
- 警告通知消息,显示在界面中间,带有普通消息文本,点击时消失。 -
WARNING_HTML
- 警告通知消息,显示在界面中间,带有 HTML 消息文本,点击时消失。 -
ERROR
- 错误通知消息,显示在界面中间,带有普通消息文本,点击时消失。 -
ERROR_HTML
- 错误通知消息,显示在界面中间,带有 HTML 消息文本,点击时消失。
默认值:
TRAY
.配置接口:
ClientConfig
可以在 Web 客户端 block 使用。
-
- cuba.hasMultipleTableConstraintDependency
-
针对实体组合采用
JOINED
的继承策略。如果设置为true
,为在数据库插入新实体提供正确的顺序。默认值:
false
- cuba.healthCheckResponse
-
- cuba.httpSessionExpirationTimeoutSec
-
定义 HTTP 会话非活动状态的超时时限,单位为秒
默认值:
1800
配置接口:
WebConfig
可以在 web 客户端 block 使用。
推荐对于 cuba.userSessionExpirationTimeoutSec 和
cuba.httpSessionExpirationTimeoutSec
属性使用相同的值。
- cuba.iconsConfig
- cuba.inMemoryDistinct
-
启用基于内存的重复记录过滤,而不使用数据库级别的
select distinct
。用在 DataManager 中。默认值:
false
保存在数据库。
配置接口:
ServerConfig
可以在 Middleware block 使用。
- cuba.jmxUserLogin
-
- cuba.keyForSecurityTokenEncryption
-
作为实体安全令牌(security token)AES 加密的密钥。当实体通过下面方式在 Middleware 加载的时候,这个令牌会放置在实体实例内发送:
-
cuba.entityAttributePermissionChecking 应用程序属性设置成 true,也就是说在 Middleware 对实体属性权限进行了加强。(参考 DataManager 安全机制 了解详情)
-
行级的安全限制过滤了集合一组属性中的部分元素。这种情况下,安全令牌也会包含在 REST API 的返回 JSON 中,参考 集合属性的安全约束。
-
实体设置了动态 属性访问控制。
尽管安全令牌不包含任何属性值(只有属性名称和过滤了的实体标识符),仍然高度建议在生产环境中修改默认的加密密钥值。
默认值:
CUBA.Platform
配置接口:
ServerConfig
可以在 Middleware block 使用。
-
- cuba.numberIdCacheSize
-
当继承了
BaseLongIdEntity
或者BaseIntegerIdEntity
的实体实例通过Metadata.create()
方法在内存创建的时候,会给创建的实体分配一个唯一标识符。这个值通过从数据序列取下一个值的机制得到的。为了减少对 Middleware 和数据库调用的次数,序列每次的增加值默认是设置的 100,也就是说平台每次从数据库调用一次能获取 100 个 id。也就是说按照这种方式“缓存”了序列值,直到 100 个 id 用完之前都可以直接从内存获取 id。这个属性定义了每次序列自增的值,以及对应的内存中缓存的大小。
如果在数据库已经有部分实体存在的情况下需要修改这个属性的值,此时会重新创建所有已经存在的序列:用新的自增值(必须等于
cuba.numberIdCacheSize
),起始值是目前已经存在 id 的最大值。别忘了在应用的所有 block 都设置这个属性。比如,如果有 Web 客户端,Portal 客户端和 Middleware,需要在
web-app.properties
,portal-app.properties
和app.properties
中将这个属性设置成相同的值。默认值: 100
配置接口:
GlobalConfig
在所有标准的 blocks 中都可以使用。
- cuba.legacyPasswordEncryptionModule
-
与cuba.passwordEncryptionModule 相同,但这个配置是用于为在旧版本中(版本 7 之前)创建并且
SEC_USER.PASSWORD_ENCRYPTION
字段为空的用户定义用于用户密码哈希的 bean 的名称。默认值:
cuba_Sha1EncryptionModule
用于所有所标准 block。
- cuba.localeSelectVisible
-
登录时禁用用户界面语言选择。
如果
cuba.localeSelectVisible
设置成 false,用户会话的 locale 会按照下面方式选择:-
如果
User
实例定义了language
属性,系统会使用这个属性定义的语言。 -
如果用户的操作系统语言在可选的区域列表里(通过 cuba.availableLocales 设置),系统会使用这个语言。
-
其它情况下,系统会使用定义在 cuba.availableLocales 属性中的第一个语言。
默认值:
true
配置接口:
GlobalConfig
在所有标准的 blocks 中都可以使用。
-
- cuba.logDir
-
对于 Tomcat 快速部署,默认值:
${catalina.home}/logs
,指向tomcat/logs
目录。对于 WAR 和 UberJAR 部署情况:
${app.home}/logs
,指向应用程序根目录下的logs
目录。配置接口:
GlobalConfig
在所有标准的 blocks 中都可以使用。
- cuba.mainMessagePack
- cuba.maxUploadSizeMb
- cuba.metadataConfig
-
累加属性,定义 metadata.xml 文件。
平台使用 Resource - 资源接口来加载此文件,所以这个文件可以放在 classpath 或者配置文件目录。
在所有标准的 blocks 中都可以使用。
示例:
cuba.metadataConfig = +com/company/sample/metadata.xml
- cuba.passwordEncryptionModule
-
定义用作用户密码 Hash 的 bean 名称。创建新用户或更新用户密码时,将在
SEC_USER.PASSWORD_ENCRYPTION
数据库字段中为该用户存储此属性的值。默认值:
cuba_BCryptEncryptionModule
在所有标准的 blocks 中都可以使用。
- cuba.passwordPolicyEnabled
-
启用强制密码策略。如果此属性设置为
true
,所有新的用户密码都会按照 cuba.passwordPolicyRegExp 属性定义的策略检查。默认值:
false
保存在数据库。
配置接口:
ClientConfig
使用在所有的客户端 blocks:web 客户端,web Portal。
- cuba.passwordPolicyRegExp
-
定义一个正则表达式,用来做密码检查策略。
默认值:
((?=.*\\d)(?=.*\\p{javaLowerCase}) (?=.*\\p{javaUpperCase}).{6,20})
上面这个表达式确保密码需要包含 6~20 个字符,使用数字和英文字母,包含至少一个数字,一个小写字母,一个大写字母。更多关于正则表达式语法可以参考 https://en.wikipedia.org/wiki/Regular_expression 和 http://docs.oracle.com/javase/6/docs/api/java/util/regex/Pattern.html 。
保存在数据库。
配置接口:
ClientConfig
使用在所有的客户端 blocks:web 客户端,web Portal。
- cuba.performanceLogDisabled
-
如果需要禁用
PerformanceLogInterceptor
,此参数必须要设置为true
。PerformanceLogInterceptor
通过类或者方法的@PerformanceLog
注解触发,此拦截器会在perfstat.log
文件中记录每次方法的调用记录以及执行时间。如果不需要这些日志,建议禁用PerformanceLogInterceptor
以提高性能。如需重新启用,删除此参数或者设置为false
。默认值:
false
可以在 Middleware block 使用。
- cuba.performanceTestMode
-
应用程序在做性能测试的时候必须设置成 true。
配置接口:
GlobalConfig
默认值:
false
可以用在 Middleware 和 web 客户端。
- cuba.permissionConfig
-
累加属性,用来定义 permissions.xml 文件。
可以在 Web 客户端 block 使用。
示例:
cuba.permissionConfig = +com/company/sample/web-permissions.xml
- cuba.persistenceConfig
-
累加属性,用来定义 persistence.xml 文件。
平台使用 Resource - 资源接口来加载此文件,所以这个文件可以放在 classpath 或者配置文件目录。
在所有标准的 blocks 中都可以使用。
示例:
cuba.persistenceConfig = +com/company/sample/persistence.xml
- cuba.portal.anonymousUserLogin
-
定义在 Web Portal block 可以做匿名用户会话的用户名称。
此属性设置的用户名需要在安全子弟痛存在,并且有需要的权限。不需要为此用户设置密码,因为匿名用户会话是通过 loginTrusted() 方法创建的,使用的是 cuba.trustedClientPassword 属性定义的密码。
配置接口:
PortalConfig
可以在 Web Portal block 使用。
- cuba.queryCache.enabled
-
- cuba.queryCache.maxSize
-
设置查询缓存实体数量的最大值。一条缓存记录是通过查询语句文本,查询语句参数,分页参数以及软删除配置确定。
由于缓存大小会慢慢增长到最大值,所以缓存机制会清除掉那些不大可能会被再次使用的记录。
默认值: 100
配置接口:
QueryCacheConfig
可以在 Middleware block 使用。
- cuba.remotingSpringContextConfig
-
累加属性,用来定义 Middleware block 的 remoting-spring.xml 文件。
平台使用 Resource - 资源接口来加载此文件,所以这个文件可以放在 classpath 或者配置文件目录。
可以在 Middleware block 使用。
示例:
cuba.remotingSpringContextConfig = +com/company/sample/remoting-spring.xml
- cuba.rest.allowedOrigins
-
- cuba.rest.anonymousEnabled
-
- cuba.rest.client.id
-
定义 REST API 客户端的标识符。客户端,在这里,并不是说平台的用户,而是一个使用 REST API 的应用(某些 web portal 或者手机 app)。使用客户端用户名密码做访问 REST API token 端点的基本认证。
默认值:
client
可以在 Web 客户端和 Web Portal blocks 使用。
- cuba.rest.client.secret
-
定义 REST API 客户端的密码。客户端,在这里,并不是说平台的用户,而是一个使用 REST API 的应用(某些 web portal 或者手机 app)。使用客户端用户名密码做访问 REST API token 端点的基本认证。应用程序属性的值需要在具体的密码值(比如
secret
)之前包含一个PasswordEncoder
的标识符(比如{noop}
)。默认值:
{noop}secret
可以在 Web 客户端和 Web Portal blocks 使用。
- cuba.rest.client.tokenExpirationTimeSec
-
- cuba.rest.client.refreshTokenExpirationTimeSec
-
- cuba.rest.deleteExpiredTokensCron
-
指定用来做定时任务的表达式,从数据库删除过期的令牌。
默认值:
0 0 3 * * ?
可以在 Middleware block 使用。
- cuba.rest.jsonTransformationConfig
-
平台使用 Resource - 资源接口来加载此文件,所以这个文件可以放在 classpath 或者配置文件目录。
文件的 XSD schema: http://schemas.haulmont.com/cuba/7.0/rest-json-transformations.xsd.
默认值: none
示例:
cuba.rest.jsonTransformationConfig = +com/company/sample/json-transformations.xml
可以在 Web 客户端和 Web Portal blocks 使用。
- cuba.rest.maxUploadSize
-
- cuba.rest.optimisticLockingEnabled
-
如果在 JSON 中提供了实体的
version
属性,启用Versioned
实体的乐观锁。默认值:
false
可以在 Web 客户端和 Web Portal blocks 使用。
- cuba.rest.requiresSecurityToken
-
如果是 true,会在加载的实体中包含一个特殊的系统属性,并且需要在保存实体的时候给 REST 接口传回一个同样的属性。细节参考 集合属性的安全约束。
默认值:
false
可以在 Web 客户端和 Web Portal blocks 使用。
- cuba.rest.reuseRefreshToken
-
定义是否可以重复使用刷新令牌。如果设置为
false
,当使用刷新令牌获取访问令牌的时候,系统会发出一个新的刷新令牌,老的刷新令牌将会失效。默认值:
true
可以在 Web 客户端和 Web Portal blocks 使用。
- cuba.rest.servicesConfig
-
平台使用 Resource - 资源接口来加载此文件,所以这个文件可以放在 classpath 或者配置文件目录。
文件的 XSD schema: http://schemas.haulmont.com/cuba/7.0/rest-services-v2.xsd.
默认值: none
示例:
cuba.rest.servicesConfig = +com/company/sample/app-rest-services.xml
可以在 Web 客户端和 Web Portal blocks 使用。
- cuba.rest.storeTokensInDb
-
启用在数据库保存 REST API 安全令牌。默认情况下,令牌只是在内存中存储。
保存在数据库。
配置接口:
ServerConfig
默认值:
false
可以在 Middleware block 使用。
- cuba.rest.tokenMaskingEnabled
-
设置是否在应用程序日志中对 REST API 进行掩码处理。
默认值:
true
可以在 Web 客户端和 Web Portal blocks 使用。
- cuba.rest.queriesConfig
-
平台使用 Resource - 资源接口来加载此文件,所以这个文件可以放在 classpath 或者配置文件目录。
文件的 XSD schema: http://schemas.haulmont.com/cuba/7.0/rest-queries.xsd.
默认值: none
示例:
cuba.rest.queriesConfig = +com/company/sample/app-rest-queries.xml
可以在 Web 客户端和 Web Portal blocks 使用。
- cuba.schedulingActive
-
- cuba.serialization.impl
-
指定
Serialization
接口的实现类,用来在应用程序 blocks 之间做对象传递时做序列化。平台包含两个实现类:-
com.haulmont.cuba.core.sys.serialization.StandardSerialization
- 标准 Java 序列化。 -
com.haulmont.cuba.core.sys.serialization.KryoSerialization
- 基于 Kryo 框架的序列化实现。
默认值:
com.haulmont.cuba.core.sys.serialization.StandardSerialization
在所有标准的 blocks 中都可以使用。
-
- cuba.springContextConfig
-
累加属性,用来为各个 block 定义 spring.xml 文件。
平台使用 Resource - 资源接口来加载此文件,所以这个文件可以放在 classpath 或者配置文件目录。
在所有标准的 blocks 中都可以使用。
示例:
cuba.springContextConfig = +com/company/sample/spring.xml
- cuba.supportEmail
-
定义一个 email 地址。从默认异常处理界面发送的异常报告,从 Help > Feedback 界面发送的用户消息都会被发送到这个地址。
如果这个属性没有设置,系统会隐藏异常处理界面的 Report 按钮。
为了成功的发送邮件,配置电子邮件发送参数 中描述的参数也必须设置。
默认值: empty string.
保存在数据库。
配置接口:
WebConfig
可以在 web 客户端 block 使用。
- cuba.tempDir
-
对于 Tomcat 快速部署,默认值:
${catalina.home}/temp/${cuba.webContextName}
,指向tomcat/temp
目录下 web app 名称的目录,比如tomcat/temp/app-core
。对于 WAR 和 UberJAR 部署情况:
${app.home}/${cuba.webContextName}/temp
,指向应用程序根目录下的一个目录。配置接口:
GlobalConfig
在所有标准的 blocks 中都可以使用。
- cuba.testMode
-
如果在运行自动化用户界面测试,这个属性必须设置成 true。
配置接口:
GlobalConfig
默认值:
false
可以用在 Web 客户端和 Middleware blcoks。
- cuba.themeConfig
-
定义一组
*-theme.properties
文件,存储 theme 变量,比如默认的对话框范围以及文本输入框的宽度。这个属性的值是一组用空格分隔的文件列表,文件通过使用 Resource - 资源接口来加载。
Web 客户端的默认值:
com/haulmont/cuba/havana-theme.properties com/haulmont/cuba/halo-theme.properties com/haulmont/cuba/hover-theme.properties
可以在 Web 客户端使用。
- cuba.triggerFilesCheck
-
启用对 bean 调用触发器文件的处理。
触发器文件是放在应用程序 block 的临时文件夹的
triggers
子目录。触发器文件命名是包含使用“点“分隔的两部分。前半部分是 bean 名称,后半部分是 bean 中需要调用的方法名称。示例:cuba_Messages.clearCache
。触发器文件处理器会监控这个目录是否有新文件,如果有的话,会调用指定的方法,然后删除这些文件。默认情况下,触发器文件的处理是配置在
cuba-web-spring.xml
文件中,并且只在 Web 客户端配置。在项目级别,其它模块的处理可以通过周期性的调用cuba_TriggerFilesProcessor
bean 的process()
方法来做。默认值:
true
可以在配置了需要处理的 blocks 中使用,默认是 web 客户端。
- cuba.triggerFilesCheckInterval
-
- cuba.trustedClientPassword
-
定义
LoginService.loginTrusted()
方法要使用的密码。Middleware 层可以使用这个密码而不是用户密码来做用户认证,这些用户通过信任的客户端 block 连接。这个属性在用户的密码不存在数据库的时候使用,客户端 block 会自己做实际的认证。比如,集成 Active Directory 的时候。
配置接口:
ServerConfig
,WebAuthConfig
,PortalConfig
可以使用的 blocks:Middleware,Web 客户端,Web Portal。
- cuba.trustedClientPermittedIpList
-
定义一组 IP 地址,从这些地址可以调用
LoginService.loginTrusted()
方法。示例:cuba.trustedClientPermittedIpList = 127.0.0.1, 10.17.*.*
默认值:
127.0.0.1
配置接口:
ServerConfig
可以在 Middleware block 使用。
- cuba.uniqueConstraintViolationPattern
-
正则表达式,用来找出由于数据库违反唯一性约束造成的异常。约束的名称会从表达式的第一个非空组获得,示例:
ERROR: duplicate key value violates unique constraint "(.+)"
约束的名称可以用来显示本地化消息,以便找出是什么实体引起的。因此,需要在主语言包中包含为约束名定义的键值。示例:
IDX_SEC_USER_UNIQ_LOGIN = A user with the same login already exists
根据 DBMS locale 和版本的不同,这个属性还可以定义针对违反唯一性约束需要作出的响应。
当前 DBMS 的默认值通过
PersistenceManagerService.getUniqueConstraintViolationPattern()
方法返回。可以定义在数据库。
可以在所有的客户端 blocks 使用。
- cuba.useCurrentTxForConfigEntityLoad
-
对于通过配置接口加载实体实例,如果当前已经有事务了,则使用当前事务加载。这个对性能有提高。否则,会创建一个新的连接,并且做提交,然后会返回游离(detached)的实体实例。
默认值:
false
可以在 Middleware block 使用。
- cuba.useEntityDataStoreForIdSequence
-
如果此属性设置为 true,为
BaseLongIdEntity
和BaseIntegerIdEntity
子类生成唯一标识符的序列会创建在相应实体存在的数据存储中。否则,这些序列存在主数据存储中。默认值:
false
配置接口:
ServerConfig
可以在 Middleware block 使用。
- cuba.useInnerJoinOnClause
-
EclipseLink ORM 会在 inner join 中使用
JOIN ON
从句,而不会在WHERE
从句中使用条件语句。默认值: false
可以在 Middleware block 使用。
- cuba.useLocalServiceInvocation
- cuba.useReadOnlyTransactionForLoad
-
- cuba.user.fullNamePattern
-
为用户定义显示全部名称的模式。
默认值:
{FF| }{LL}
全名称模式可以用用户的姓,名,和中间名(外国人)来构成。模式需要按照下面的规则:
-
模式的各部分通过
{}
来分隔。 -
在
{}
内的部分必须包含一下列出的字符的一种,然后紧跟一个|
,没有空格:LL
– 用户姓的长格式 (Smith)L
– 用户姓的短格式 (S)FF
– 用户名的长格式 (John)F
– 用户名的短格式 (J)MM
– 用户中间名的长格式 (Paul)M
– 用户中间名的短格式 (P) -
|
字符后面可以跟任何符号,也可以包含空格。
可以在 Web 客户端 block 使用。
-
- cuba.user.namePattern
-
为
User
实体定义显示名称模式。显示名称用在几个不同的地方,包括显示在系统主窗口右上角。默认值:
{1} [{0}]
{0}
会用login
属性替换,{1}
会用name
属性替换。可以在 Middleware,Web 客户端 blocks 使用。
- cuba.userSessionExpirationTimeoutSec
-
定义用户会话超时的时限。单位是秒。
默认值:
1800
配置接口:
ServerConfig
可以在 Middleware block 使用。
建议为
cuba.userSessionExpirationTimeoutSec
和 cuba.httpSessionExpirationTimeoutSec 设置相同的值。
- cuba.userSessionProviderUrl
-
定义 Middleware block 的 URL 来作为用户登录服务。
这个参数需要在额外的 middleware blcoks 中设置,这些 middleware 可以执行客户端请求,但是不分享用户会话缓存。如果在请求发起的时候,在本地的缓存中没有需要的会话,这个 block 会在指定的 URL 执行
LoginService.getSession()
方法,然后将取到的会话缓存到本地。配置接口:
ServerConfig
可以在 Middleware block 使用。
- cuba.viewsConfig
- cuba.webAppUrl
- cuba.web.urlHandlingMode
-
定义如何处理 URL 更改。
可选值为
UrlHandlingMode
枚举类型的元素:-
NONE
– 不会处理 URL 改动; -
BACK_ONLY
– 使用CubaHistoryControl
来处理改动。这个值替换旧的cuba.web.allowHandleBrowserHistoryBack属性; -
URL_ROUTES
– 使用URL 历史及导航功能处理改动。
默认值:
URL_ROUTES
配置接口:
WebConfig
-
- cuba.windowConfig
-
累加属性,用来定义 screens.xml 文件。
Resource - 资源接口来加载此文件,所以这个文件可以放在 classpath 或者配置文件目录。
可以在 Web 客户端 block 使用。
示例:
cuba.windowConfig = +com/company/sample/web-screens.xml
- cuba.web.allowHandleBrowserHistoryBack
-
如果登录界面和/或者主窗口实现了
CubaHistoryControl.HistoryBackHandler
接口的话,用此参数开启对于浏览器 Back - 返回 按钮的处理。如果此属性设置为 true,浏览器标准的行为会替换成调用接口的处理方法。参考 Web 客户端基础设施。
默认值:
true
配置接口:
WebConfig
可以在 web 客户端 block 使用。
- cuba.web.appFoldersRefreshPeriodSec
-
- cuba.web.appWindowMode
-
定义主应用窗口的初始化模式 - “标签式”还是“单页式”(
TABBED
或者SINGLE
)。在“单页式”模式下,当使用NEW_TAB
参数打开界面时,新界面会完全替换现有界面而不是打开一个新的标签页。用户之后可以在 Help > Settings 界面更改此模式。
默认值:
TABBED
配置接口:
WebConfig
可以在 web 客户端 block 使用。
- cuba.web.closeIdleHttpSessions
- cuba.web.componentsConfig
-
累加属性,用来定义包含应用程序组件信息的配置文件,这些文件由不同的 Jar 包提供或者在 web 模块的
cuba-ui-component.xml
中描述。示例:
cuba.web.componentsConfig =+demo-web-components.xml
- cuba.web.customDeviceWidthForViewport
-
自定义 HTML 页面的 viewport 宽度。影响 Vaadin HTML 页面上的 "viewport" 元标签(viewport meta tag)。
默认值:
-1
配置接口:
WebConfig
可以在 web 客户端 block 使用。
- cuba.web.defaultScreenCanBeClosed
-
定义默认界面是否也可以通过关闭按钮、ESC 按钮或者标签页右键菜单(当使用
TABBED
工作区模式)进行关闭。默认值: true
配置接口:
WebConfig
可以在 web 客户端 block 使用。
- cuba.web.defaultScreenId
-
定义登录后默认打开的界面。这个设置对所有用户生效。
示例:
cuba.web.defaultScreenId = sys$SendingMessage.browse
配置接口:
WebConfig
可以在 web 客户端 block 使用。
- cuba.web.externalAuthentication
-
Deprecated - 废弃. 使用 web 登录扩展点替代.
通过外部认证机制进行认证,比如 LDAP 或者 SSO IDP。也可参考cuba.web.externalAuthenticationProviderClass以及 IDP 插件 wiki 。
默认值:
false
配置接口:
WebAuthConfig
可以在 web 客户端 block 使用。
- cuba.web.externalAuthenticationProviderClass
-
Deprecated - 废弃. 使用 web 登录扩展点替代.
实现了
CubaAuthProvider
接口的类,当 cuba.web.externalAuthentication 设置成 true 的时候会使用这个类做认证。参考 集成 LDAP 和 IDP 插件 wiki 了解更多示例。
配置接口:
WebAuthConfig
可以在 web 客户端 block 使用。
- cuba.web.foldersPaneDefaultWidth
-
- cuba.web.foldersPaneEnabled
-
启用文件夹面板功能。
默认值:
false
配置接口:
WebConfig
可以在 web 客户端 block 使用。
- cuba.web.foldersPaneVisibleByDefault
-
- cuba.web.ldap.enabled
-
在 Web 客户端启用/禁用 LDAP 登录机制。
示例:
cuba.web.ldap.enabled = true
配置接口:
WebLdapConfig
可以在 web 客户端 block 使用。
- cuba.web.ldap.urls
-
设置 LDAP 服务器 URL。
示例:
cuba.web.ldap.urls = ldap://192.168.1.1:389
配置接口:
WebLdapConfig
可以在 web 客户端 block 使用。
- cuba.web.ldap.base
-
为在 LDAP 中搜索用户设置基本域名称(DN)。
示例:
cuba.web.ldap.base = ou=Employees,dc=mycompany,dc=com
配置接口:
WebLdapConfig
可以在 web 客户端 block 使用。
- cuba.web.ldap.user
-
系统用户别名,系统用户有权限从目录读取信息。
示例:
cuba.web.ldap.user = cn=System User,ou=Employees,dc=mycompany,dc=com
配置接口:
WebLdapConfig
可以在 web 客户端 block 使用。
- cuba.web.ldap.password
-
cuba.web.ldap.user 属性定义的系统用户的密码。
示例:
cuba.web.ldap.password = system_user_password
配置接口:
WebLdapConfig
可以在 web 客户端 block 使用。
- cuba.web.ldap.userLoginField
-
LDAP 用户的用来匹配登录名的属性名称。默认是
sAMAccountName
(使用于 Active Directory)。示例:
cuba.web.ldap.userLoginField = username
配置接口:
WebLdapConfig
可以在 web 客户端 block 使用。
- cuba.web.linkHandlerActions
-
定义一组可以由
LinkHandler
bean 处理的 URL 命令。参考 界面链接 了解更多信息。值需要用
|
字符分隔。默认值:
open|o
配置接口:
WebConfig
可以在 web 客户端 block 使用。
- cuba.web.loginDialogDefaultUser
-
设置默认的用户名称,会在登录窗口自动填充。这个在开发阶段会非常有用。这个属性在生产环境需要设置成
<disabled>
的值。默认值:
admin
配置接口:
WebConfig
可以在 web 客户端 block 使用。
- cuba.web.loginDialogDefaultPassword
-
设置默认的用户密码,会在登录窗口自动填充。这个在开发阶段会非常有用。这个属性在生产环境需要设置成
<disabled>
的值。默认值:
admin
配置接口:
WebConfig
可以在 web 客户端 block 使用。
- cuba.web.loginDialogPoweredByLinkVisible
-
设置成
false
在登录窗口隐藏 "powered by CUBA Platform"。默认值:
true
配置接口:
WebConfig
可以在 web 客户端 block 使用。
- cuba.web.mainTabSheetMode
-
定义在主窗口的 标签页模式使用哪个组件管理界面。可以选取
MainTabSheetMode
枚举类型中的一个值:-
DEFAULT
:使用CubaTabSheet
组件。会在每次用户切换标签页的时候加载和卸载界面组件。 -
MANAGED
: 使用CubaManagedTabSheet
组件。这个不会在用户切换标签页的时候卸载界面组件。
默认值:
DEFAULT
.配置接口:
WebConfig
.可以在 web 客户端 block 使用。
-
- cuba.web.managedMainTabSheetMode
-
如果 cuba.web.mainTabSheetMode 属性设置为
MANAGED
,定义主 TabSheet 怎么切换标签页:隐藏还是只卸载它们的组件。默认值:
HIDE_TABS
配置接口:
WebConfig
可以在 web 客户端 block 使用。
- cuba.web.maxTabCount
-
定义可以在应用程序主窗口打开的标签页的最大数量。
0
值表示不限制。默认值:
7
配置接口:
WebConfig
可以在 web 客户端 block 使用。
- cuba.web.pageInitialScale
-
定义当设置了 cuba.web.customDeviceWidthForViewport 或者 cuba.web.useDeviceWidthForViewport 是
true
的时候 HTML 界面的初始化缩放比。影响 Vaadin HTML 界面的 "viewport" 元标签。默认值:
0.8
配置接口:
WebConfig
可以在 web 客户端 block 使用。
- cuba.web.productionMode
-
可以完全关闭通过在 URL 中添加
?debug
打开的 Vaadin 开发者模式。因此,也同时关闭了 JavaScript 调试模式,减少了从浏览器获取的服务器信息。默认值:
false
配置接口:
WebConfig
可以在 web 客户端 block 使用。
- cuba.web.pushLongPolling
-
- cuba.web.pushLongPollingSuspendTimeoutMs
-
定义服务器推送的超时时限,单位是毫秒。当服务器推送实现切换成长轮询的时候会用到。比如当
cuba.web.pushLongPolling="true"
时。默认值:
-1
配置接口:
WebConfig
可以在 Web 客户端使用。
- cuba.web.rememberMeEnabled
-
在 web 客户端的登录界面显示 Remember Me - 记住我 复选框。
默认值:
true
配置接口:
WebConfig
可以在 Web 客户端使用。
- cuba.web.resourcesCacheTime
-
启用是否缓存 web 资源。单位是秒。0 值表示不做缓存。示例:
cuba.web.resourcesCacheTime = 136
默认值: 60 * 60 (1 小时).
配置接口:
WebConfig
可以在 Web 客户端使用。
- cuba.web.webJarResourcesCacheTime
-
启用是否缓存 WebJar 资源。单位是秒。0 值表示不做缓存。示例:
cuba.web.webJarResourcesCacheTime = 631
默认值: 60 * 60 * 24 * 365 (1 年).
配置接口:
WebConfig
可以在 Web 客户端使用。
- cuba.web.resourcesRoot
-
设置用来显示 Embedded 组件的文件目录。示例:
cuba.web.resourcesRoot = ${cuba.confDir}/resources
默认值:
null
配置接口:
WebConfig
可以在 Web 客户端使用。
- cuba.web.requirePasswordForNewUsers
-
如果设置为
true
,在 Web 客户端创建用户的时候需要密码。如果是使用 LDAP 认证的话,建议将此参数设置为false
。默认值:
true
配置接口:
WebAuthConfig
可以在 web 客户端 block 使用。
- cuba.web.showBreadCrumbs
-
设置是否在主窗口的工作区显示面包屑(breadcrumbs)面板。
默认值:
true
配置接口:
WebConfig
可以在 web 客户端 block 使用。
- cuba.web.showFolderIcons
-
启用文件夹面板图标。当启用时,会使用下面这些样式文件:
-
icons/app-folder-small.png
– 用于应用目录 -
icons/search-folder-small.png
– 用户查找目录 -
icons/set-small.png
– 用于记录集合
默认值:
false
配置接口:
WebConfig
可以在 web 客户端 block 使用。
-
- cuba.web.standardAuthenticationUsers
- cuba.web.table.cacheRate
-
调整网页浏览器中的 Table 缓存。缓存行的数量是由
cacheRate
* pageLength 的值决定。默认值:
2
配置接口:
WebConfig
可以在 web 客户端 block 使用。
- cuba.web.table.pageLength
-
当 Table 刷新第一次渲染的时候,设置从服务端获取数据的行数。也可参考 cuba.web.table.cacheRate。
默认值:
15
配置接口:
WebConfig
可以在 web 客户端 block 使用。
- cuba.web.theme
-
- cuba.web.uiHeartbeatIntervalSec
-
定义 Web 客户端用户界面心跳请求的间隔。如果没设置,则会使用 cuba.httpSessionExpirationTimeoutSec / 3。
默认值: HTTP-session 非活动状态超时时限 / 3,单位为秒
配置接口:
WebConfig
可以在 web 客户端 block 使用。
- cuba.web.useFontIcons
-
如果在 Halo 主题中开启了这个属性,Font Awesome 的象形图标会被用来作为平台界面标准行为的图标,而不是使用图片。
可视化组件或者操作的 icon 属性和字体元素之间的关联关系通过平台的
halo-theme.properties
文件定义。以cuba.web.icons
为前缀的键值对应图标名称,然后它们的值,用com.vaadin.server.FontAwesome
枚举类型常量定义。比如,标准create
动作的字体元素定义如下:cuba.web.icons.create.png = font-icon:FILE_O
默认值:
true
配置接口:
WebConfig
可以在 web 客户端 block 使用。
- cuba.web.unsupportedPagePath
-
定义HTML 界面的路径,这个界面用来在应用程序不支持当前浏览器版本时显示。
cuba.web.unsupportedPagePath = /com/company/sales/web/sys/unsupported-browser-page.html
默认值:
/com/haulmont/cuba/web/sys/unsupported-page-template.html
配置接口:
WebConfig
可以在 web 客户端 block 使用。
- cuba.web.useInverseHeader
- cuba.web.userCanChooseDefaultScreen
-
Defines whether a user is able to choose the default screen. If the
false
value is set, the Default screen field in the Settings screen is read-only.默认值: true
配置接口:
WebConfig
可以在 web 客户端 block 使用。
- cuba.web.useDeviceWidthForViewport
-
处理 viewport 的宽度。如果需要使用设备宽度作为 viewport 宽度时设置为
true
。这个属性影响 Vaadin HTML 界面 viewport 的元标签。默认值:
false
配置接口:
WebConfig
可以在 web 客户端 block 使用。
- cuba.web.viewFileExtensions
-
当使用
ExportDisplay.show()
方法下载文件时,定义一组浏览器直接显示的文件后缀名的列表。使用|
字符来分隔列表中的后缀名。默认值:
htm|html|jpg|png|jpeg|pdf
配置接口:
WebConfig
可以在 web 客户端 block 使用。
- cuba.webContextName
-
定义 web 应用的上下文(context)名称。通常跟包含此应用程序 block 的目录或者 WAR 文件同名。
配置接口:
GlobalConfig
可以使用的 blocks:Middleware,Web 客户端,Web Portal。
比如,对 Middleware block 来说,如果放置的目录是
tomcat/webapps/app-core
,并且可以通过http://somehost:8080/app-core
访问,则此属性应该设置为:cuba.webContextName = app-core
- cuba.webHostName
-
定义运行应用程序 block 的机器主机名称。
默认值:
localhost
配置接口:
GlobalConfig
可以使用的 blocks:Middleware,Web 客户端,Web Portal。
比如,对 Middleware block 来说,可以通过
http://somehost:8080/app-core
访问,则此属性应该设置为:cuba.webHostName = somehost
- cuba.webPort
-
定义运行应用程序 block 的端口。
默认值:
8080
配置接口:
GlobalConfig
可以使用的 blocks:Middleware,Web 客户端,Web Portal。
比如,对 Middleware block 来说,可以通过
http://somehost:8080/app-core
访问,则此属性应该设置为:cuba.webPort = 8080
Appendix C: 系统参数
系统参数可以在 JVM 启动的时候通过命令行参数 -D
指定。此外,系统参数可以通过 System
类的 getProperty()
和 setProperty()
方法来读写。
可以用系统参数来设置或者覆盖应用程序属性的值。比如,下面命令行参数就会覆盖 cuba.connectionUrlList 属性的值,这个属性一般在 web-app.properties
文件中设置:
-Dcuba.connectionUrlList=http://somehost:8080/app-core
牢记于心,系统参数是会影响整个 JVM 的,也就是说,在此 JVM 上运行的所有 blocks 对于同一个参数,都读取的同一个值。 |
框架会在服务启动时缓存系统参数,所以应用程序不应该依赖在运行时修改系统参数来改变应用程序属性。如果确实需要这么做,使用 CachingFacadeMBean JMX bean 的 |
以下是平台使用的系统参数,这些参数不是应用程序属性。
- logback.configurationFile
-
定义 Logback 框架配置文件位置。
对于运行在 Tomcat web 服务的应用程序 blocks,这个系统参数配置在
tomcat/bin/setenv.bat
和tomcat/bin/setenv.sh
文件。默认值是指向tomcat/conf/logback.xml
配置文件。
- cuba.unitTestMode
-
在
CubaTestCase
基类运行集成测试的时候,这个系统参数设置成true
。示例:
if (!Boolean.valueOf(System.getProperty("cuba.unitTestMode"))) return "Not in test mode";
Appendix D: 移除的部分
D.1. 组织业务逻辑
参阅 在 CUBA 中创建业务逻辑 向导。
D.2. 控制器中的业务逻辑
参阅 在 CUBA 中创建业务逻辑 向导。
D.3. 使用客户端层 Beans
参阅 在 CUBA 中创建业务逻辑 向导。
D.4. 使用中间件服务
参阅 在 CUBA 中创建业务逻辑 向导。
D.5. 使用实体监听器
示例请参阅 实体监听器 部分。
D.6. 使用 JMX Beans
示例请参阅 创建 JMX Bean 部分。
D.7. 在程序启动时执行代码
示例参阅注册实体监听器部分。
D.8. 使用通用 UI
参阅 通用用户界面(GUI)。
D.9. Web 应用程序主题
参阅 主题。
D.10. 从 Havana 迁移至功能丰富的 Halo 主题
示例请参阅修改主题通用参数部分。
D.15. 多对多关联
参阅 数据模型:多对多关联 向导。
D.16. 直接多对多关联
参阅 直接多对多关联 向导。
D.17. 使用关联实体进行多对多关联
参阅 使用关联实体进行间接多对多关联 向导。
D.18. 实体继承
参阅 数据模型:实体继承 向导。
8. 术语表
- Application Tiers - 应用分层
-
参考 应用程序层和块。
- Artifact - 工件
-
在本手册的上下文中,工件是指一个文件(通常是 JAR 或者 ZIP 文件)包含了可执行代码或者构建项目生成的其它代码。工件具有按照特定规则定义的名称和版本号,并且可以保存在工件仓库里。
- Artifact Repository - 工件仓库
-
按特定目录结构保存 artifacts 的服务器,当项目启动构建时,项目依赖的工件都从这个仓库加载。
- Base Projects - 基本项目
-
跟应用程序组件一个意思,这个术语在之前版本的平台和文档中使用。
- Container - 容器
-
容器控制应用程序里面对象的生命周期以及配置。这个是依赖注入(反转控制)机制的基本组件。
CUBA 框架使用 Spring 框架 的容器。
- DB - 数据库
-
关系型数据库。
- Datasource - 数据源
-
参考 数据源(历史版本).
- Dependency Injection - 依赖注入
-
也称反转控制(IoC)。一个用来获取使用的对象链接的机制,这个机制会假设一个对象只需要声明它依赖的那些对象,而由容器来帮助创建所有需要的对象并将这些对象注入到依赖他们的对象中去。
- Eager Fetching - 预加载
-
跟请求的实体一起加载子类实体以及关联实体的数据。
- Entity - 实体
-
数据模型的主要元素,参考 数据模型。
- Entity Browser - 实体浏览界面
-
包含一个用来显示实体列表的表格以及一些用来创建、编辑和删除实体的按钮的界面。
- EntityManager
-
中间件层的组件,用来持久化实体。
参考 EntityManager.
- Interceptor - 拦截器
-
面向切面编程的一个概念,可以用来改变或者扩展对象方法的执行过程。
- JMX
-
Java Management Extensions − 提供工具来管理应用、系统对象和设备的一种技术。为 JMX-components 定义了标准。
也可参考 使用 JMX 工具。
- JPA
-
Java Persistence API – ORM 技术的一套标准规范。CUBA 框架使用实现了此规范的 EclipseLink 框架。
- JPQL
-
平台独立的面向对象的查询语言,是 JPA 规范的一部分。
- Lazy loading - 懒加载
-
参考 延迟加载.
- Local attribute - 本地属性
-
实体的属性,此属性不关联其它实体,也不是其它实体的集合。典型情况下,实体的所有本地属性都存在数据库的一张表内(当然不包括特定的实体继承的情况)。
- Localized message pack - 本地化语言消息包
-
参考 语言消息包。翻译时有时候简称语言包,本地化语言包等。
- Managed Beans - 托管 bean
-
包含应用程序业务逻辑的组件。
参考 托管 Bean.
- Main Message Pack - 主语言消息包
-
参考 主语言消息包.
- MBeans
-
带有 JMX 接口的托管 bean。典型情况下,这些 bean 包含一个内部状态(比如可以是缓存,配置数据或者统计数据),这个内部状态需要通过 JMX 访问。
- Middleware - 中间件
-
中间层 – 包含业务逻辑的应用程序层,跟数据库通信并且为更高的客户端层提供接口通用接口服务。有时候不翻译,有时候翻译成中间件或者中间层。
- Optimistic locking - 乐观锁
-
乐观锁 - 用来管理不同用户访问共享数据的一种方式,这里假设不同用户对同一实体实例只有非常低的可能性进行并发访问。采用这种方案并不会真正意义上对数据加锁,而是在数据发生变动时检查在数据库是否存在更新版本的数据。如果存在,则会抛出异常,用户需要重新加载实体。
- ORM
-
对象关系映射 – 将关系型数据库的表跟编程语言中对象进行关联的一种技术。
参考 ORM 层.
- Persistent context - 持久化上下文
-
一组从数据库加载的或者刚创建的实体实例。持久化上下文作为当前事务的数据缓存。当事务提交时,所有持久化上下文内的实体变更都被保存到数据库。
参考 EntityManager.
- XML-descriptor - 界面 XML 描述
-
包含界面可视化组件布局和数据源的 XML 文件。
参考 界面 XML 描述.