这是关于lumos项目核心的二次开发文档.

1. 环境安装

1.1. 软件需求

  1. 开发集成环境(Eclipse,IDEA)

  2. Maven3.6

1.2. Maven配置

setting 文件配置
<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xmlns="http://maven.apache.org/SETTINGS/1.0.0"
          xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
    <pluginGroups>
    </pluginGroups>

    <proxies>
    </proxies>

    <servers>
        <server>
            <id>amaxgs-libs-release</id>
            <username>amaxgs</username>
            <password>AP3nYPUJA2wZ6gHfhoq2Ar6wwXd</password>
        </server>
        <server>
            <id>amaxgs-libs-snapshot</id>
            <username>amaxgs</username>
            <password>AP3nYPUJA2wZ6gHfhoq2Ar6wwXd</password>
        </server>
        <server>
            <id>ags-central</id>
            <username>amaxgs</username>
            <password>AP3nYPUJA2wZ6gHfhoq2Ar6wwXd</password>
        </server>
    </servers>

    <mirrors>
        <mirror>
            <id>ags-central</id>
            <name>ags-central</name>
            <mirrorOf>central</mirrorOf>
            <url>http://119.119.118.149:8081/artifactory/remote-central</url>
        </mirror>
        <mirror>
            <id>ags-vaadin-addons</id>
            <name>ags-vaadin-addons</name>
            <mirrorOf>vaadin-addons</mirrorOf>
            <url>http://119.119.118.149:8081/artifactory/remote-vaadin-addons</url>
        </mirror>
    </mirrors>

    <profiles>
        <profile>
            <id>amaxgs-libs</id>
            <repositories>
                <repository>
                    <snapshots>
                        <enabled>false</enabled>
                    </snapshots>
                    <id>amaxgs-libs-release</id>
                    <name>libs-release</name>
                    <url>http://119.119.118.149:8081/artifactory/libs-release</url>
                </repository>
                <repository>
                    <id>amaxgs-libs-snapshot</id>
                    <name>libs-snapshot</name>
                    <url>http://119.119.118.149:8081/artifactory/libs-snapshot</url>
                </repository>
            </repositories>
        </profile>

    </profiles>

    <activeProfiles>
        <activeProfile>amaxgs-libs</activeProfile>
    </activeProfiles>

</settings>

2. 名词介绍

Abbreviation Comment

JSH

Jasper Service Hub,特指Jasper后台服务,完成特定的功能,不需要页面交互,比如数据监控,第三方系统数据同步等,可以作为windows服务存在。

3. 软件架构介绍

3.1. 平台优势

  • 基于网页B/S架构设计,升级方便,使用简单。

  • 支持多语言。

  • 拖拽式的工艺路线设计,可视化的建模场景,从具体到抽象,从繁琐到简单。

  • 基于云架构的应用设计,采用私有云+公有云的部署方式,将传统与现代相结合,生产中赋予智慧。

  • 平台化产品,对建模的对象,如物料等,支持属性扩展,以满足不同行业的发展需要。

  • 支持报表扩展。用户可通过报表工具自主研发报表,并与平台无缝集成。

  • 以配置驱动功能的扩展,避免大量的二次开发工作。

  • 模块独立,可按照实际要求逐步实施,满足中小企业灵活多变的业务需求。

  • 与众多软件提供商集,可适配市面上大部分硬件,为生产智能化打下基础。

  • 原生报表丰富,从设计到生产看板,正追踪,反追溯等个用户对于生产流程的把控细致入微

3.2. 平台逻辑架构图

core\architecture

整体分为3层架构,分别为逻辑处理层,中间适配(sdk)层,前端表现层。

3.2.1. 逻辑处理层

所有跟业务逻辑的处理会全部放在这一层,包含以下几个方面:

  • 表结构的设计,数据持久化

  • Entity对象的设计

  • 业务逻辑处理,专注于业务逻辑的实现,是Jasper平台的核心。如过站逻辑,质量卡控,工艺路线定制等的处理。

  • 权限的卡控

  • RPC接口的发布。平台的所有接口,会以Dubbo技术对外发布RPC接口,作为平台服务提供的手段。

3.2.2. 中间适配层(SDK)

该层提供对服务端RPC接口的封装,用于前端以及其他第三方模块接入系统的入口。主要包含以下几方面:

  • 以Service封装服务端的Handler接口

  • 前端对象封装,提供对Entity对象的扩展

  • Restful 接口,作为其他Html5产品接入的接口

3.2.3. 前端表现层

前端表现,不单单是指传统的HTML5页面,还有诸如Mobile,JSH服务等,所有需要通过SDK接入系统的服务,全部都在这一层。

目前前端已经基于Vaadin构建了一整套技术框架,可以快速实现功能,快速部署。

3.3. 平台二次开发架构

下面从多个维度,不同实现复杂度来阐述二次开发的架构

core\framework 1
core\framework 2
core\framework 3
core\framework 4

3.4. 平台部署架构

lumos可以采用多种方式来部署:

  • 针对小企业,无集群要求,可以尽可能节省硬件资源,全部放在一台服务器。比较推荐的配置为2台服务器,数据库和应用分开。

  • 针对稍微大型企业,有集群要求,则可以借助于前后端分离的方式部署

  • 针对巨无霸企业,有集群要求,业务量巨大,并发数巨大,可以采用更高级的方式部署

以下为各种情况下的部署示意图

core\deploy 1
core\deploy 2
core\deploy 3

4. 开始一个项目

4.1. 项目结构

本文档只是提供一个项目的推荐样例结构,具体的项目结构可根据实际需求自行调整。

样例项目结构如下:

demo-parent:父项目,主要有项目依赖管理、插件管理以及参数定义。
    |---- demo-sdk:主要有实体类、客户端对象、Service接口及Handler接口的定义
    |---- demo-impl:主要有DAO、Handler实现类以及LiquiBase的初始化脚本
    |---- demo-web:主要有页面代码
    |---- demo-app:主要有启动类及配置信息
lumos平台统一使用Maven作为项目管理工具。
LiquiBase作为数据库脚本管理工具。

4.2. 创建父项目

创建Maven父项目lumos-demo-parent,pom文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <!-- 父项目为lumosframework的父项目,其中包含lumosframework项目模块的依赖管理 -->
    <parent>
        <groupId>com.ags.lumosframework</groupId>
        <artifactId>lumos-parent</artifactId>
        <version>3.1.0-SNAPSHOT</version>
    </parent>

    <artifactId>lumos-demo-parent</artifactId>
    <version>3.1.0-SNAPSHOT</version>
    <packaging>pom</packaging>

    <modules>
        <module>demo-sdk</module>
        <module>demo-impl</module>
        <module>demo-web</module>
        <module>demo-app</module>
        <module>demo-rpc-app</module>
        <module>demo-rpc-withregistry-app</module>
    </modules>

    <distributionManagement>
        <!-- 如果是在外地(非内网环境)编码,注意更改地址。 -->
        <snapshotRepository>
            <id>amaxgs-libs-snapshot</id>
            <url>http://119.119.118.149:8081/artifactory/ags-protected-snapshots-local</url>
        </snapshotRepository>
    </distributionManagement>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.ags.lumosframework</groupId>
                <artifactId>lumos-demo-sdk</artifactId>
                <version>${project.version}</version>
            </dependency>
            <dependency>
                <groupId>com.ags.lumosframework</groupId>
                <artifactId>lumos-demo-impl</artifactId>
                <version>${project.version}</version>
            </dependency>
            <dependency>
                <groupId>com.ags.lumosframework</groupId>
                <artifactId>lumos-demo-web</artifactId>
                <version>${project.version}</version>
            </dependency>
            <dependency>
                <groupId>com.ags.lumosframework</groupId>
                <artifactId>lumos-demo-app</artifactId>
                <version>${project.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>utf-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

4.3. 创建SDK项目

创建子项目lumos-demo-sdk,pom文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.ags.lumosframework</groupId>
        <artifactId>lumos-demo-parent</artifactId>
        <version>3.1.0-SNAPSHOT</version>
    </parent>

    <artifactId>lumos-demo-sdk</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.ags.lumosframework</groupId>
            <artifactId>lumos-sdk</artifactId>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>
</project>

4.4. 创建Impl项目

创建子项目lumos-demo-impl,pom文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>com.ags.lumosframework</groupId>
        <artifactId>lumos-demo-parent</artifactId>
        <version>3.1.0-SNAPSHOT</version>
    </parent>
    <artifactId>lumos-demo-impl</artifactId>

    <dependencies>
        <!-- 包含lumos平台自带对象的DAO及Handler实现,此处引用其DAO及Handler实现类的基类 -->
        <dependency>
            <groupId>com.ags.lumosframework</groupId>
            <artifactId>lumos-impl</artifactId>
        </dependency>
        <dependency>
            <groupId>com.ags.lumosframework</groupId>
            <artifactId>lumos-demo-sdk</artifactId>
        </dependency>
    </dependencies>
</project>

4.5. 创建web项目

创建子项目lumos-demo-web,pom文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>com.ags.lumosframework</groupId>
        <artifactId>lumos-demo-parent</artifactId>
        <version>3.1.0-SNAPSHOT</version>
    </parent>

    <artifactId>lumos-demo-web</artifactId>

    <dependencies>
        <!-- 包含页面的基础组件 -->
        <dependency>
            <groupId>com.ags.lumosframework</groupId>
            <artifactId>lumos-web-vaadin</artifactId>
        </dependency>
        <dependency>
            <groupId>com.ags.lumosframework</groupId>
            <artifactId>lumos-demo-sdk</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <!-- 使用Vaadin作为前端框架需要加入此插件用来编译Widgetset -->
            <plugin>
                <groupId>com.vaadin</groupId>
                <artifactId>vaadin-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>update-widgetset</goal>
                            <goal>compile</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

4.6. 创建最终打包APP项目

4.6.1. 单体应用

创建子项目lumos-demo-app,pom文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>com.ags.lumosframework</groupId>
        <artifactId>lumos-demo-parent</artifactId>
        <version>3.1.0-SNAPSHOT</version>
    </parent>
    <artifactId>lumos-demo-app</artifactId>

    <dependencies>
        <dependency>
            <groupId>com.ags.lumosframework</groupId>
            <artifactId>lumos-impl</artifactId>
        </dependency>
        <dependency>
            <groupId>com.ags.lumosframework</groupId>
            <artifactId>lumos-demo-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.ags.lumosframework</groupId>
            <artifactId>lumos-demo-impl</artifactId>
        </dependency>
    </dependencies>
</project>

添加启动类DemoApp:

package com.ags.lumosframework;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;

@SpringBootApplication(exclude = HibernateJpaAutoConfiguration.class)
public class DemoApp {

    public static void main(String[] args) {
        SpringApplication.run(DemoApp.class, args);
    }

}

在resources目录下添加配置文件application.properties

spring.application.name=demo-app
spring.main.allow-bean-definition-overriding=true
# \u6307\u5B9A\u8BBF\u95EE\u7CFB\u7EDF\u7684\u6839\u8DEF\u5F84
server.servlet.context-path=/demo
dubbo.protocol.port=20880
dubbo.registry.address=N/A
# \u6570\u636E\u5E93\u914D\u7F6E
lumos.commons.data.datasource.driver=com.mysql.jdbc.Driver
lumos.commons.data.hibernate-properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
lumos.commons.data.datasource.url=jdbc:mysql://localhost:3306/lumos_demo
lumos.commons.data.datasource.username=root
lumos.commons.data.datasource.password=123456
lumos.commons.data.hibernate-properties.show_sql=true
file.size=10

项目结构如下:

project

选中demo-parent项目,执行maven package,启动入口DemoApp类。启动完成后, 访问URL:http://localhost:8080/demo,页面如下,初始管理员用户名及密码为admin/P@ssw0rd。 至此,项目搭建完成。

logon
Figure 1. 登录页面
home
Figure 2. 首页

4.6.2. 前后端单独启动(使用RPC接口)

  1. 创建lumos-demo-backend-app,pom文件如下:

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <parent>
            <artifactId>lumos-demo-rpc-app</artifactId>
            <groupId>com.ags.lumosframework</groupId>
            <version>3.1.0-SNAPSHOT</version>
        </parent>
        <modelVersion>4.0.0</modelVersion>
    
        <artifactId>lumos-demo-backend-app</artifactId>
    
        <dependencies>
            <dependency>
                <groupId>com.ags.lumosframework</groupId>
                <artifactId>lumos-demo-impl</artifactId>
            </dependency>
        </dependencies>
    
    </project>

    添加启动类DemoBackendApp,

    package com.ags.lumosframework;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
    
    @SpringBootApplication(exclude = HibernateJpaAutoConfiguration.class)
    public class DemoBackendApp {
    
        public static void main(String[] args) {
            SpringApplication.run(DemoBackendApp.class, args);
        }
    
    }

    在resources目录下添加配置文件application.properties

    spring.application.name=demo-app
    spring.main.allow-bean-definition-overriding=true
    spring.cloud.service-registry.auto-registration.enabled=false
    dubbo.protocol.port=20880
    dubbo.registry.address=N/A
    lumos.commons.data.datasource.driver=com.mysql.jdbc.Driver
    lumos.commons.data.hibernate-properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
    lumos.commons.data.datasource.url=jdbc:mysql://localhost:3306/colin_mes
    lumos.commons.data.datasource.username=root
    lumos.commons.data.datasource.password=123456
    lumos.commons.data.hibernate-properties.hibernate.show_sql=true
    lumos.commons.data.hibernate-properties.hibernate.cache.use_second_level_cache=true
    lumos.commons.data.hibernate-properties.hibernate.cache.use_query_cache=true
    lumos.commons.data.hibernate-properties.javax.persistence.sharedCache.mode=ENABLE_SELECTIVE
    lumos.commons.data.hibernate-properties.hibernate.cache.region.factory_class=com.ags.lumosframework.impl.base.hibernate.internal.EhcacheRegionFactory
    lumos.commons.data.datasource-hikari.transaction-isolation=TRANSACTION_READ_COMMITTED
    
    file.size=10
  2. 创建lumos-demo-frontend-app,pom文件如下:

    <?xml version="1.0" encoding="UTF-8"?>
    
    <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <parent>
            <artifactId>lumos-demo-rpc-app</artifactId>
            <groupId>com.ags.lumosframework</groupId>
            <version>3.1.0-SNAPSHOT</version>
        </parent>
        <modelVersion>4.0.0</modelVersion>
    
        <artifactId>lumos-demo-frontend-app</artifactId>
    
        <dependencies>
            <dependency>
                <groupId>com.ags.lumosframework</groupId>
                <artifactId>lumos-demo-web</artifactId>
            </dependency>
        </dependencies>
    
    </project>

    添加启动类DemoFrontendApp,

    package com.ags.lumosframework;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    public class DemoFrontendApp {
    
        public static void main(String[] args) {
            SpringApplication.run(DemoFrontendApp.class, args);
        }
    
    }

    在resources目录下添加配置文件application.properties

    spring.application.name=demo-app
    spring.main.allow-bean-definition-overriding=true
    server.servlet.context-path=/demo
    dubbo.consumer.url=119.119.118.152:20880
    dubbo.registry.address=N/A
    dubbo.provider.export=false
    file.size=10

    项目结构如下:

    project rpc
  3. 选中demo-parent项目,执行maven package,先启动DemoBackendApp,完成后再启动DemoFrontendApp, 启动完成后,访问URL:http://localhost:8080/demo,初始管理员用户名及密码为admin/P@ssw0rd。

示例只是演示RPC接口的使用,在实际项目中可以将前后端一起打包成单体应用,其他模块使用RPC接口调用服务。

4.6.3. 多节点(使用服务注册中心)

  1. 下载zookeeper并启动。(附下载地址 mirror.bit.edu.cn/apache/zookeeper/ )

  2. 创建lumos-demo-withregistry-backend-app项目,pom文件如下:

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <parent>
            <artifactId>lumos-demo-rpc-withregistry-app</artifactId>
            <groupId>com.ags.lumosframework</groupId>
            <version>3.1.0-SNAPSHOT</version>
        </parent>
        <modelVersion>4.0.0</modelVersion>
    
        <artifactId>lumos-demo-withregistry-backend-app</artifactId>
    
        <dependencies>
            <dependency>
                <groupId>com.ags.lumosframework</groupId>
                <artifactId>lumos-demo-impl</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
            </dependency>
        </dependencies>
    
    </project>

    添加启动类:

    package com.ags.lumosframework;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
    
    @SpringBootApplication(exclude = HibernateJpaAutoConfiguration.class)
    public class DemoBackendWithRegistryApp {
    
        public static void main(String[] args) {
            SpringApplication.run(DemoBackendWithRegistryApp.class, args);
        }
    
    }

    在resources目录下添加配置文件application.properties:

    spring.application.name=demo-backend-app
    spring.main.allow-bean-definition-overriding=true
    spring.cloud.zookeeper.connect-string=localhost:2181
    
    dubbo.protocol.name=dubbo
    dubbo.protocol.port=-1
    dubbo.registry.address=spring-cloud://localhost
    dubbo.provider.timeout=3000000
    lumos.commons.data.datasource.driver=com.mysql.jdbc.Driver
    lumos.commons.data.hibernate-properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect
    lumos.commons.data.datasource.url=jdbc:mysql://localhost:3306/colin_mes
    lumos.commons.data.datasource.username=root
    lumos.commons.data.datasource.password=123456
    lumos.commons.data.hibernate-properties.show_sql=true
    file.size=10
  3. 创建lumos-demo-withregistry-frontend-app项目,pom文件如下:

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <parent>
            <artifactId>lumos-demo-rpc-withregistry-app</artifactId>
            <groupId>com.ags.lumosframework</groupId>
            <version>3.1.0-SNAPSHOT</version>
        </parent>
        <modelVersion>4.0.0</modelVersion>
    
        <artifactId>lumos-demo-withregistry-frontend-app</artifactId>
    
        <dependencies>
            <dependency>
                <groupId>com.ags.lumosframework</groupId>
                <artifactId>lumos-demo-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
            </dependency>
        </dependencies>
    
    </project>

    添加启动类:

    package com.ags.lumosframework;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    public class DemoFrontendWithRegistryApp {
    
        public static void main(String[] args) {
            SpringApplication.run(DemoFrontendWithRegistryApp.class, args);
        }
    
    }

    在resources目录下添加配置文件application.properties:

    spring.application.name=demo-frontend-app
    spring.main.allow-bean-definition-overriding=true
    spring.cloud.zookeeper.connect-string=localhost:2181
    dubbo.registry.address=spring-cloud://localhost
    server.servlet.context-path=/demo
    server.port=8086
    file.size=10

    项目结构如下:

    project rpc withregistry
  4. 选中demo-parent项目,执行maven package,先启动DemoBackendApp,完成后再启动DemoFrontendApp, 启动完成后,访问URL:http://localhost:8080/demo,初始管理员用户名及密码为admin/P@ssw0rd。

5. 实现一个对象的增删改查

该章节使用原生hibernate及LiquiBase脚本的形式实现一个对象的增删改查,如需使用平台提供的自定义表形式实现一个对象的增删改查,请查阅自定义表章节。

可使用平台提供的代码生成工具生成以下代码,详情请查阅《Code Generator Guide》。

5.1. 添加一个entity

在demo-sdk项目中添加Entity类。

package com.ags.lumosframework.demo.sdk.entity;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;

import com.ags.lumosframework.sdk.base.entity.BaseEntity;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@Entity
@Table(name = "DEMO_BOOK")
public class BookEntity extends BaseEntity {

    @Column(name = "BOOK_NAME", length = 255)
    private String name;

    @Column(name = "INTRODUCTION", length = 1024)
    private String introduction;

    @Column(name = "PRICE")
    private double price;

}

在demo-sdk项目中添加domain类(客户端对象)。

package com.ags.lumosframework.demo.sdk.domain;

import com.ags.lumosframework.demo.sdk.entity.BookEntity;
import com.ags.lumosframework.sdk.base.domain.ObjectBaseImpl;

public class Book extends ObjectBaseImpl<BookEntity> {

    public Book() {
        super(null);
    }

    public Book(BookEntity entity) {
        super(entity);
    }

    @Override
    public String getName() {
        return getInternalObject().getName();
    }

    public void setName(String name) {
        getInternalObject().setName(name);
    }

    public String getIntroduction() {
        return getInternalObject().getIntroduction();
    }

    public void setIntroduction(String introduction) {
        getInternalObject().setIntroduction(introduction);
    }

    public double getPrice() {
        return getInternalObject().getPrice();
    }

    public void setPrice(double price) {
        getInternalObject().setPrice(price);
    }

}
示例中使用lombok生成get/set方法。如果不使用lombok,则将 @Getter @Setter 注解去掉,然后手动或使用IDE将get/set方法加上。

使用lombok工具,IDE需要安装lombok插件:

  • Idea:setting → plugins → 搜索lombok plugin,然后安装。

  • Eclipse:在lombok官网下载lombok.jar,双击然后指定Eclipse安装目录安装插件。(附下载地址: projectlombok.org/download )

5.2. 添加脚本

在demo-impl项目resources目录下创建目录db-changelog/demo,
在新创建的目录下添加创建对应表的LiquiBase脚本table-ddl.xml:

table-ddl.xml
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
        xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.6.xsd">

    <changeSet author="system" id="CREATE_TABLE_DEMO_BOOK">
        <validCheckSum>any</validCheckSum>
        <createTable tableName="DEMO_BOOK">
            <!-- <<<<<<<<<<<<<<<<<<<<<公有属性开始<<<<<<<<<<<<<<<<<<<<<-->
            <column name="ID" type="bigint">
                <constraints primaryKey="true"/>
            </column>
            <column name="PLATFORM_ID" type="varchar(255)"/>
            <column name="COMPANY_ID" type="bigint"/>
            <column name="DESCRIPTION" type="varchar(2000)"/>

            <column name="CREATE_TIME" type="datetime2"/>
            <column name="CREATE_USER_ID" type="bigint"/>
            <column name="CREATE_USER_NAME" type="varchar(255)"/>
            <column name="CREATE_USER_FULL_NAME" type="varchar(255)"/>
            <column name="CREATE_IP" type="varchar(128)"/>
            <column name="DTS_CREATION_BID" type="bigint"/>

            <column name="LM_TIME" type="datetime2"/>
            <column name="LM_USER_ID" type="bigint"/>
            <column name="LM_IP" type="varchar(128)"/>
            <column name="LM_USER_NAME" type="varchar(255)"/>
            <column name="LM_USER_FULL_NAME" type="varchar(255)"/>
            <column name="DTS_MODIFIED_BID" type="bigint"/>

            <column name="DELETE_TIME" type="datetime2"/>
            <column name="DELETE_USER_ID" type="bigint"/>
            <column name="DELETE_USER_NAME" type="varchar(255)"/>
            <column name="DELETE_USER_FULL_NAME" type="varchar(255)"/>
            <column name="DELETE_IP" type="varchar(128)"/>
            <column name="DELETED" type="boolean" defaultValue="false"/>

            <column name="ROW_LOG_ID" type="varchar(255)"/>
            <!-->>>>>>>>>>>>>>>>>>>>>>>公有属性结束>>>>>>>>>>>>>>>>>>>>>>>-->
            <column name="BOOK_NAME" type="varchar(255)"/>
            <column name="INTRODUCTION" type="varchar(1024)"/>
            <column name="PRICE" type="number(19, 6)"/>
        </createTable>
    </changeSet>
    <changeSet author="system" id="DEMO_BOOK_ADD_INDEX_NAME">
        <createIndex indexName="IDX_DEMO_BOOK_NAME" tableName="DEMO_BOOK">
            <column name="BOOK_NAME"/>
        </createIndex>
    </changeSet>

</databaseChangeLog>
该示例创建了四个脚本以放置不同类型的脚本。
db changelog
changelog.xml
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
                   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.6.xsd">

    <!-- 引入建表脚本 -->
    <include file="table-ddl.xml" relativeToChangelogFile="true"/>
    <!-- 引入数据初始化脚本 -->
    <include file="data-dml.xml" relativeToChangelogFile="true"/>
    <!-- 引入补充脚本;一般地,在应用上线后,不能直接修改原有脚本,而是通过补充脚本来修改数据表结构及数据 -->
    <include file="patch.xml" relativeToChangelogFile="true"/>

</databaseChangeLog>

5.3. 添加dao

在demo-impl项目中添加DAO类。

package com.ags.lumosframework.demo.impl.dao;

import org.springframework.stereotype.Repository;

import com.ags.lumosframework.demo.sdk.entity.BookEntity;
import com.ags.lumosframework.impl.base.dao.core.BaseEntityDao;

@Repository("bookDao")
public class BookDao extends BaseEntityDao<BookEntity> {}

在添加了DAO类后,需要让其受Spring管理,步骤如下:

  1. 在demo-impl项目中添加Spring的Config类:

    package com.ags.lumosframework.demo.impl.config;
    
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    
    import com.ags.lumosframework.impl.base.autoconfig.CommonsDataConfigurer;
    
    @Configuration
    @ComponentScan(basePackages = {"com.ags.lumosframework.demo.impl.dao", "com.ags.lumosframework.demo.impl.handler"})
    @CommonsDataConfigurer(entityPackages = {"com.ags.lumosframework.demo.sdk.entity"},
        changelog = "db-changelog/demo/changelog.xml", order = 10)
    public class DemoImplAutoConfig {}
    其中注解 @Configuration 指定其为配置类,
    注解 @ComponentScan 指定Spring需要扫描的包,
    注解 @CommonsDataConfigurer 指定Entity所在的包及LiquiBase脚本的路径。
  2. 在demo-impl项目的resources/META-INF目录下创建spring.factories文件:

    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    com.ags.lumosframework.demo.impl.config.DemoImplAutoConfig
每个有受Spring管理的Bean的项目都需要在项目中指定需要扫描的包或类。

5.4. 添加handler

在demo-sdk项目中添加handler接口。

package com.ags.lumosframework.demo.sdk.handler;

import com.ags.lumosframework.demo.sdk.entity.BookEntity;
import com.ags.lumosframework.sdk.base.handler.api.IBaseEntityHandler;
import com.ags.lumosframework.sdk.dubbo.RpcSupport;

@RpcSupport
public interface IBookHandler extends IBaseEntityHandler<BookEntity> {

}

在demo-impl项目中添加handler接口的实现类。

package com.ags.lumosframework.demo.impl.handler;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;

import com.ags.lumosframework.demo.impl.dao.BookDao;
import com.ags.lumosframework.demo.sdk.entity.BookEntity;
import com.ags.lumosframework.demo.sdk.handler.IBookHandler;
import com.ags.lumosframework.impl.base.dao.core.BaseEntityDao;
import com.ags.lumosframework.impl.base.handler.AbstractBaseEntityHandler;

@Service
@Primary
public class BookHandler extends AbstractBaseEntityHandler<BookEntity> implements IBookHandler {

    @Autowired
    private BookDao bookDao;

    @Override
    protected BaseEntityDao<BookEntity> getDao() {
        return bookDao;
    }

}

5.5. 添加service

在demo-sdk项目中添加service接口。

package com.ags.lumosframework.demo.sdk.service;

import com.ags.lumosframework.demo.sdk.domain.Book;
import com.ags.lumosframework.sdk.base.service.api.IBaseDomainObjectService;

public interface IBookService extends IBaseDomainObjectService<Book> {

}

在demo-sdk中添加service接口的实现。

package com.ags.lumosframework.demo.sdk.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;

import com.ags.lumosframework.demo.sdk.domain.Book;
import com.ags.lumosframework.demo.sdk.entity.BookEntity;
import com.ags.lumosframework.demo.sdk.handler.IBookHandler;
import com.ags.lumosframework.demo.sdk.service.IBookService;
import com.ags.lumosframework.sdk.base.handler.api.IBaseEntityHandler;
import com.ags.lumosframework.sdk.base.service.AbstractBaseDomainObjectService;

@Service
@Primary
public class BookService extends AbstractBaseDomainObjectService<Book, BookEntity> implements IBookService {

    @Autowired
    private IBookHandler bookHandler;

    @Override
    protected IBaseEntityHandler<BookEntity> getEntityHandler() {
        return bookHandler;
    }

}

在添加了Service实现类后,需要让其受Spring管理,步骤如下:

  1. 在demo-sdk项目中添加Spring的Config类:

    package com.ags.lumosframework.demo.sdk.config;
    
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    
    import com.ags.lumosframework.sdk.dubbo.RpcServiceScan;
    
    @Configuration
    @ComponentScan(basePackages = {"com.ags.lumosframework.demo.sdk.service.impl"})
    @RpcServiceScan({"com.ags.lumosframework.demo.sdk.handler"})
    public class DemoSDKAutoConfig {}
  2. 在demo-sdk项目的resources/META-INF目录下创建spring.factories文件:

    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    com.ags.lumosframework.demo.sdk.config.DemoSDKAutoConfig
每个有受Spring管理的Bean的项目都需要在项目中指定需要扫描的包或类。

5.6. 添加ui

在demo-web项目中添加UI。

package com.ags.lumosframework.web.ui;

import com.ags.lumosframework.web.vaadin.base.BaseUIHasMenu;
import com.ags.lumosframework.web.vaadin.base.annotation.WebEntry;
import com.vaadin.annotations.Theme;
import com.vaadin.spring.annotation.SpringUI;

@WebEntry(shortCaption = "Demo", longCaption = "示例", description = "This is a demo module", iconPath = "images/mes.png",
    order = 1)
@SpringUI(path = "Demo")
@Theme("light")
public class DemoUI extends BaseUIHasMenu {

    @Override
    protected void setTitle() {

    }
}

指定需要扫描的包或类。

  1. 在demo-web项目中添加Spring的Config类:

    package com.ags.lumosframework.web.config;
    
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    @ComponentScan(basePackages = {"com.ags.lumosframework.web.ui", "com.ags.lumosframework.web.i18n"})
    public class DemoWebAutoConfig {}
  2. 在demo-web项目的resources/META-INF目录下创建spring.factories文件:

    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    com.ags.lumosframework.web.config.DemoWebAutoConfig
每个有受Spring管理的Bean的项目都需要在项目中指定需要扫描的包或类。

5.7. 添加view

在demo-web项目中添加View。

package com.ags.lumosframework.web.ui.view.demo;

import com.ags.lumosframework.common.exception.PlatformException;
import com.ags.lumosframework.demo.sdk.domain.Book;
import com.ags.lumosframework.demo.sdk.service.IBookService;
import com.ags.lumosframework.web.common.i18.I18NUtility;
import com.ags.lumosframework.web.common.i18.I18Support;
import com.ags.lumosframework.web.common.security.annotation.Secured;
import com.ags.lumosframework.web.constant.DemoPermissionConstants;
import com.ags.lumosframework.web.ui.DemoUI;
import com.ags.lumosframework.web.vaadin.base.BaseView;
import com.ags.lumosframework.web.vaadin.base.ConfirmDialog;
import com.ags.lumosframework.web.vaadin.base.ConfirmResult;
import com.ags.lumosframework.web.vaadin.base.CoreTheme;
import com.ags.lumosframework.web.vaadin.base.annotation.Menu;
import com.ags.lumosframework.web.vaadin.component.paginationobjectlist.IDomainObjectGrid;
import com.ags.lumosframework.web.vaadin.component.paginationobjectlist.PaginationDomainObjectList;
import com.vaadin.icons.VaadinIcons;
import com.vaadin.navigator.ViewChangeListener;
import com.vaadin.spring.annotation.SpringView;
import com.vaadin.ui.Button;
import com.vaadin.ui.Component;
import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.VerticalLayout;
import org.springframework.beans.factory.annotation.Autowired;

import java.text.NumberFormat;
import java.util.Optional;

@Menu(caption = "Book", captionI18NKey = "demo.book.view.caption", iconPath = "images/icon/text-blob.png", order = 0)
@SpringView(name = "Book", ui = DemoUI.class)
public class BookView extends BaseView implements Button.ClickListener {

    /**
     *
     */
    private static final long serialVersionUID = 8141409383766735767L;

    @Secured(DemoPermissionConstants.BOOK_ADD)
    @I18Support(caption = "Add", captionKey = "common.add")
    private Button btnAdd = new Button();

    @Secured(DemoPermissionConstants.BOOK_EDIT)
    @I18Support(caption = "Edit", captionKey = "common.edit")
    private Button btnEdit = new Button();

    @Secured(DemoPermissionConstants.BOOK_DELETE)
    @I18Support(caption = "Delete", captionKey = "common.delete")
    private Button btnDelete = new Button();

    @Secured(DemoPermissionConstants.BOOK_REFRESH)
    @I18Support(caption = "Refresh", captionKey = "common.refresh")
    private Button btnRefresh = new Button();

    private Button[] btns = new Button[] {btnAdd, btnEdit, btnDelete, btnRefresh};

    private HorizontalLayout hlToolBox = new HorizontalLayout();

    private IDomainObjectGrid<Book> objectGrid = new PaginationDomainObjectList<>();

    @Autowired
    private AddBookDialog addBookDialog;

    @Autowired
    private IBookService bookService;

    public BookView() {
        VerticalLayout vlRoot = new VerticalLayout();
        vlRoot.setMargin(false);
        vlRoot.setSizeFull();

        hlToolBox.setWidth("100%");
        hlToolBox.addStyleName(CoreTheme.TOOLBOX);
        hlToolBox.setMargin(true);
        vlRoot.addComponent(hlToolBox);
        HorizontalLayout hlTempToolBox = new HorizontalLayout();
        hlToolBox.addComponent(hlTempToolBox);
        for (Button btn : btns) {
            hlTempToolBox.addComponent(btn);
            btn.addClickListener(this);
            btn.setDisableOnClick(true);
        }
        btnAdd.setIcon(VaadinIcons.PLUS);
        btnEdit.setIcon(VaadinIcons.EDIT);
        btnDelete.setIcon(VaadinIcons.TRASH);
        btnRefresh.setIcon(VaadinIcons.REFRESH);

        objectGrid.addColumn(Book::getName).setCaption(I18NUtility.getValue("demo.book.name", "Name"));
        objectGrid.addColumn(Book::getIntroduction)
            .setCaption(I18NUtility.getValue("demo.book.introduction", "Introduction"));
        objectGrid.addColumn(source -> {
            double price = source.getPrice();
            return NumberFormat.getInstance().format(price);
        }).setCaption(I18NUtility.getValue("demo.book.price", "Price"));
        objectGrid.setObjectSelectionListener(event -> {
            setButtonStatus(event.getFirstSelectedItem());
        });
        vlRoot.addComponents((Component)objectGrid);
        vlRoot.setExpandRatio((Component)objectGrid, 1);

        this.setSizeFull();
        this.setCompositionRoot(vlRoot);
    }

    private void setButtonStatus(Optional<Book> optional) {
        boolean enable = optional.isPresent();
        btnEdit.setEnabled(enable);
        btnDelete.setEnabled(enable);
    }

    @Override
    protected void init() {
        objectGrid.setServiceClass(IBookService.class);
    }

    @Override
    public void enter(ViewChangeListener.ViewChangeEvent event) {
        setButtonStatus(Optional.empty());
        objectGrid.refresh();
    }

    @Override
    public void buttonClick(Button.ClickEvent event) {
        Button button = event.getButton();
        button.setEnabled(true);
        if (btnAdd.equals(button)) {
            addBookDialog.setObject(null);
            addBookDialog.show(getUI(), result -> {
                if (ConfirmResult.Result.OK.equals(result.getResult())) {
                    objectGrid.refresh();
                }
            });
        } else if (btnEdit.equals(button)) {
            Book book = (Book)objectGrid.getSelectedObject();
            addBookDialog.setObject(book);
            addBookDialog.show(getUI(), result -> {
                if (ConfirmResult.Result.OK.equals(result.getResult())) {
                    Book temp = (Book)result.getObj();
                    objectGrid.refresh(temp);
                }
            });
        } else if (btnDelete.equals(button)) {
            ConfirmDialog.show(getUI(),
                I18NUtility.getValue("common.suretodelete", "Are you sure to delete the selected item?"), result -> {
                    if (ConfirmResult.Result.OK.equals(result.getResult())) {
                        try {
                            bookService.delete((Book)objectGrid.getSelectedObject());
                        } catch (PlatformException e) {
                            notificationError("Common.RelationShipCheckFailed", e.getMessage());
                            return;
                        }
                        objectGrid.refresh();
                    }
                });
        } else if (btnRefresh.equals(button)) {
            objectGrid.refresh();
        }
    }
}

继续添加Dialog类用于添加对象。

package com.ags.lumosframework.web.ui.view.demo;

import org.springframework.context.annotation.Scope;

import com.ags.lumosframework.demo.sdk.domain.Book;
import com.ags.lumosframework.demo.sdk.service.IBookService;
import com.ags.lumosframework.web.common.i18.I18NUtility;
import com.ags.lumosframework.web.common.i18.I18Support;
import com.ags.lumosframework.web.vaadin.base.BaseDialog;
import com.ags.lumosframework.web.vaadin.base.DialogCallBack;
import com.ags.lumosframework.web.vaadin.constants.VaadinCommonConstant;
import com.vaadin.data.Binder;
import com.vaadin.data.converter.StringToDoubleConverter;
import com.vaadin.data.validator.DoubleRangeValidator;
import com.vaadin.spring.annotation.SpringComponent;
import com.vaadin.ui.Component;
import com.vaadin.ui.TextField;
import com.vaadin.ui.UI;
import com.vaadin.ui.VerticalLayout;

@SpringComponent
@Scope("prototype")
public class AddBookDialog extends BaseDialog {

    @I18Support(caption = "Name", captionKey = "demo.book.name")
    private TextField tfName = new TextField();

    @I18Support(caption = "Introduction", captionKey = "demo.book.introduction")
    private TextField tfIntroduction = new TextField();

    @I18Support(caption = "Price", captionKey = "demo.book.price")
    private TextField tfPrice = new TextField();

    private Binder<Book> binder = new Binder<>();

    private String caption;

    private Book book;

    private IBookService bookService;

    public AddBookDialog(IBookService bookService) {
        this.bookService = bookService;
    }

    public void setObject(Book book) {
        String captionName = I18NUtility.getValue("demo.book.caption", "Book");
        if (book == null) {
            this.caption = I18NUtility.getValue("common.new", "New", captionName);
            book = new Book();
        } else {
            this.caption = I18NUtility.getValue("common.modify", "Modify", captionName);
        }
        this.book = book;
        binder.readBean(book);
    }

    @Override
    public void show(UI parentUI, DialogCallBack callBack) {
        setHeightUnDefinedMode();
        showDialog(parentUI, caption, VaadinCommonConstant.MEDIUM_DIALOG_WIDTH, null, false, true, callBack);
    }

    @Override
    protected void initUIData() {
        binder.forField(tfName)
            .asRequired(I18NUtility.getValue("common.requiredfilednotempty", "Required filed cannot be empty"))
            .bind(Book::getName, Book::setName);
        binder.bind(tfIntroduction, Book::getIntroduction, Book::setIntroduction);
        binder.forField(tfPrice)
            .withConverter(
                new StringToDoubleConverter(I18NUtility.getValue("Common.OnlyFloatAllowed", "Only float is allowed")))
            .withValidator(new DoubleRangeValidator(
                I18NUtility.getValue("Common.MustEqualOrLargerThan0", "Must equals or larger than 0"), 0D,
                Double.MAX_VALUE))
            .bind(Book::getPrice, Book::setPrice);
    }

    @Override
    protected void okButtonClicked() throws Exception {
        binder.writeBean(book);
        Book save = bookService.save(book);
        result.setObj(save);
    }

    @Override
    protected void cancelButtonClicked() {}

    @Override
    protected Component getDialogContent() {
        VerticalLayout vlContent = new VerticalLayout();
        vlContent.setSizeFull();

        tfName.setWidth("100%");
        tfIntroduction.setWidth("100%");
        tfPrice.setWidth("100%");

        vlContent.addComponents(tfName, tfIntroduction, tfPrice);
        return vlContent;
    }

}

5.8. 添加权限

在demo-web项目中定义权限常量:

package com.ags.lumosframework.web.constant;

public class DemoPermissionConstants {

    public static final String BOOK_ADD = "demo.book.add";
    public static final String BOOK_EDIT = "demo.book.edit";
    public static final String BOOK_DELETE = "demo.book.delete";
    public static final String BOOK_REFRESH = "demo.book.refresh";

}

在页面组件上加 @Secured 注解,参考View类中的写法。

@Secured(DemoPermissionConstants.BOOK_ADD)

在初始化脚本中加入权限相关数据

<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
        xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.6.xsd">

    <changeSet id="INIT_PERMISSION_DATA" author="system">
        <validCheckSum>any</validCheckSum>
        <insert tableName="SYS_PERMISSION">
            <column name="ID" valueNumeric="1000"/>
            <column name="COMPANY_ID" valueNumeric="-1"/>
            <column name="CREATE_TIME" valueDate="2019-06-01"/>
            <column name="CREATE_USER_ID" valueNumeric="1"/>
            <column name="CREATE_USER_NAME" value="admin"/>
            <column name="CREATE_USER_FULL_NAME" value="admin"/>
            <column name="DTS_CREATION_BID" valueNumeric="-1"/>
            <column name="LM_TIME" valueDate="2019-06-01"/>
            <column name="LM_USER_ID" valueNumeric="-1"/>
            <column name="LM_USER_NAME" value="admin"/>
            <column name="LM_USER_FULL_NAME" value="admin"/>
            <column name="DTS_MODIFIED_BID" valueNumeric="-1"/>
            <column name="DELETE_USER_ID" valueNumeric="-1"/>
            <column name="DELETED" valueBoolean="false"/>

            <column name="CATEGORY" value="Book"/>
            <column name="NAME" value="book.add"/>
            <column name="NAME_KEY" value="permission.book.add"/>
            <column name="SYSTEM_DEFINED" valueBoolean="true"/>
            <column name="MODULE" value="Demo"/>
        </insert>
        <insert tableName="SYS_PERMISSION">
            <column name="ID" valueNumeric="1001"/>
            <column name="COMPANY_ID" valueNumeric="-1"/>
            <column name="CREATE_TIME" valueDate="2019-06-01"/>
            <column name="CREATE_USER_ID" valueNumeric="1"/>
            <column name="CREATE_USER_NAME" value="admin"/>
            <column name="CREATE_USER_FULL_NAME" value="admin"/>
            <column name="DTS_CREATION_BID" valueNumeric="-1"/>
            <column name="LM_TIME" valueDate="2019-06-01"/>
            <column name="LM_USER_ID" valueNumeric="-1"/>
            <column name="LM_USER_NAME" value="admin"/>
            <column name="LM_USER_FULL_NAME" value="admin"/>
            <column name="DTS_MODIFIED_BID" valueNumeric="-1"/>
            <column name="DELETE_USER_ID" valueNumeric="-1"/>
            <column name="DELETED" valueBoolean="false"/>

            <column name="CATEGORY" value="Book"/>
            <column name="NAME" value="book.edit"/>
            <column name="NAME_KEY" value="permission.book.edit"/>
            <column name="SYSTEM_DEFINED" valueBoolean="true"/>
            <column name="MODULE" value="Demo"/>
        </insert>
        <insert tableName="SYS_PERMISSION">
            <column name="ID" valueNumeric="1002"/>
            <column name="COMPANY_ID" valueNumeric="-1"/>
            <column name="CREATE_TIME" valueDate="2019-06-01"/>
            <column name="CREATE_USER_ID" valueNumeric="1"/>
            <column name="CREATE_USER_NAME" value="admin"/>
            <column name="CREATE_USER_FULL_NAME" value="admin"/>
            <column name="DTS_CREATION_BID" valueNumeric="-1"/>
            <column name="LM_TIME" valueDate="2019-06-01"/>
            <column name="LM_USER_ID" valueNumeric="-1"/>
            <column name="LM_USER_NAME" value="admin"/>
            <column name="LM_USER_FULL_NAME" value="admin"/>
            <column name="DTS_MODIFIED_BID" valueNumeric="-1"/>
            <column name="DELETE_USER_ID" valueNumeric="-1"/>
            <column name="DELETED" valueBoolean="false"/>

            <column name="CATEGORY" value="Book"/>
            <column name="NAME" value="book.delete"/>
            <column name="NAME_KEY" value="permission.book.delete"/>
            <column name="SYSTEM_DEFINED" valueBoolean="true"/>
            <column name="MODULE" value="Demo"/>
        </insert>
        <insert tableName="SYS_PERMISSION">
            <column name="ID" valueNumeric="1003"/>
            <column name="COMPANY_ID" valueNumeric="-1"/>
            <column name="CREATE_TIME" valueDate="2019-06-01"/>
            <column name="CREATE_USER_ID" valueNumeric="1"/>
            <column name="CREATE_USER_NAME" value="admin"/>
            <column name="CREATE_USER_FULL_NAME" value="admin"/>
            <column name="DTS_CREATION_BID" valueNumeric="-1"/>
            <column name="LM_TIME" valueDate="2019-06-01"/>
            <column name="LM_USER_ID" valueNumeric="-1"/>
            <column name="LM_USER_NAME" value="admin"/>
            <column name="LM_USER_FULL_NAME" value="admin"/>
            <column name="DTS_MODIFIED_BID" valueNumeric="-1"/>
            <column name="DELETE_USER_ID" valueNumeric="-1"/>
            <column name="DELETED" valueBoolean="false"/>

            <column name="CATEGORY" value="Book"/>
            <column name="NAME" value="book.refresh"/>
            <column name="NAME_KEY" value="permission.book.refresh"/>
            <column name="SYSTEM_DEFINED" valueBoolean="true"/>
            <column name="MODULE" value="Demo"/>
        </insert>
    </changeSet>

</databaseChangeLog>

5.9. 添加国际化

平台提供了统一的国际化支持,查阅国际化支持
实现国际化只需要指定当前模块的国际化文件路径即可,步骤如下:
1、添加国际化文件Demo_I18_Normal_zh_CN.properties及Demo_I18_Normal_en_US.properties, 内容为国际化Key及Value的键值对,e.g.

demo.book.view.caption=书籍
demo.book.caption=书籍
demo.book.name=书名
demo.book.introduction=简介
demo.book.price=价格

2、在demo-web项目中添加DemoI18NProvider类,用于指定国际化文件路径。

package com.ags.lumosframework.web.i18n;

import org.springframework.stereotype.Component;

import com.ags.lumosframework.web.common.i18.I18NProvider;

@Component
public class DemoI18NProvider implements I18NProvider {

    @Override
    public String[] getBaseNames() {
        return new String[] {"locale/Demo_I18_Normal"};
    }

    @Override
    public int getPriority() {
        return 0;
    }

}

6. 国际化支持

Lumos本身提供了多种国际化方式,统一的接口,可以帮助使用者简化大量操作。

6.1. 利用国际化接口及实现

以下接口为平台提供,可以由其他模块实现的接口,如下:

package com.ags.lumosframework.web.common.i18;

public interface I18NProvider {

    /**
     * 返回该模块的国际化文件的名称。 该国际化文件一般放在 maven 项目的{reource_folder}下面
     *
     * @return
     */
    String[] getBaseNames();

    /**
     * 返回该模块的国际化文件的优先级,低优先级的会被高优先级的覆盖掉。
     *
     * @return
     */
    int getPriority();

}

6.1.1. 国际化文件声明

如果需要在新项目上增加本地化支持,需要自定义一个DemoProvider来实现接口I18Nprovider,并且实现里面的方法,getBaseNames方法是设置本地化文件的命名规则前缀。getPriority方法是设置他的优先级。

如你可以定义如下国际化文件信息

@Component
public class DemoI18NProvider implements I18NProvider {
   @Override
   public String[] getBaseNames() {
      return new String[] {"locale/Demo_I18_Normal", "locale/Demo_I18_Error"};
   }
   @Override
   public int getPriority() {
      return 1;
   }
}
  • locale/Demo_I18_Normal 和为国际化文件的地址,可以指定多个。

确保该类定义在SpringBoot的扫描路径中,否则不生效。

6.1.2. 国际化文件创建

在如下位置加入国际化文件,国际化文件统一为properties文件。

core\i18n

6.2. 使用系统对象支持国际化

除了使用国际化文件支持国际化的定义之外,Lumos也提供了I18NText对象,用户可以使用该对象对系统的国际化文本进行扩展。

core\i18neditor

如上为I18N国际化文本的定义以及在不同语言中显示的值,通过该方法的好处是用户可以在页面中直接修改,使用他们更加喜欢的值,此方式更加灵活。
在二次开发中,这个是比较推荐的一种方式。

如上样例中,我们定义一个TextId,名称为Report.Message,那么在Report页面中,当用户添加Report的时候,就可以为定制化报表自定义菜单要显示的内容。

core\i18n report

6.3. 系统使用国际化

6.3.1. vaadin控件使用国际化

vaadin的控件可以直接通过Annotation的形式直接使用国际化,目前支持注入Button,Label,TextField,ComboBox等控件。使用方式直接在类型声明上面使用如下注解即可,如:

@I18Support(caption = "Add", captionKey = "common.add")
private Button btnAdd = new Button("Add");

6.3.2. 系统信息使用国际化

如果是在处理逻辑中需要使用国际化信息,那么可以通过以下接口拿到国际化:

//主要是通过该方法拿到国际化信息
I18NUtility.getValue("Demo.Person.IdNo", "Id Card NO")

//如以下场景:
gridPersons.addColumn(Person::getIdNo).setCaption(I18NUtility.getValue("Demo.Person.IdNo", "Id Card NO"));
gridPersons.addColumn(Person::getName).setCaption(I18NUtility.getValue("Demo.Person.Name", "Name"));
gridPersons.addColumn(Person::getAge).setCaption(I18NUtility.getValue("Demo.Person.Age", "Age"));
gridPersons.addColumn(Person::getBirthday).setCaption(I18NUtility.getValue("Demo.Person.Birthday", "Birthday"));

===

7. 扩展属性

扩展属性的作用就是在原对象的基础上进行扩展,原对象缺少字段,不能满足项目的需求,这个时候可以使用扩展属性。

只有支持扩展属性的对象才能添加扩展属性,如果想让目前不支持的对象也能添加扩展属性, 则只需实现接口 CustTableColumnObjectTypeProvider ,将需要支持扩展属性的对象类型返回即可:

CustTableColumnObjectTypeProvider.java
package com.ags.lumosframework.web.vaadin.base.extension;

import java.util.List;

import com.ags.lumosframework.common.IObjectType;

/**
 * 指定自定义表Object可支持的类型<br/>
 * ComboBox选项国际化Key:customizedtable.objecttype.+类型名<br/>
 * e.g. customizedtable.objecttype.User
 */
public interface CustTableColumnObjectTypeProvider {

    /**
     * 获取支持扩展属性的对象类型集合
     *
     * @return
     */
    List<IObjectType> getObjectTypes();

}

Lumos平台提供了User,Role,Permission,Department四个对象的扩展属性支持。

LumosCustTableColumnObjectTypeProvider.java
package com.ags.lumosframework.web.vaadin.base.extension;

import java.util.Arrays;
import java.util.List;

import org.springframework.stereotype.Component;

import com.ags.lumosframework.common.IObjectType;
import com.ags.lumosframework.sdk.CoreserviceObjectType;

@Component
public class LumosCustTableColumnObjectTypeProvider implements CustTableColumnObjectTypeProvider {

    private static IObjectType objectTypes[] = {CoreserviceObjectType.User, CoreserviceObjectType.Role,
        CoreserviceObjectType.Permission, CoreserviceObjectType.Department};

    @Override
    public List<IObjectType> getObjectTypes() {
        return Arrays.asList(objectTypes);
    }

}

7.1. 系统对象扩展属性添加

在扩展属性页面添加所需对象的扩展属性,平台提供了String,Boolean,Integer,Long,Float,BigDecimal,Time,Object八种类型,其中Time类型,对应的java类型为ZonedDateTime,Object类型为一个Long类型的字段,保存对应Object的Id字段值。

AddCustomizedField
扩展属性名不能是数据库的关键字,否则添加不成功。
创建扩展属性会在数据库中创建“CF_”开头的扩展属性表。e.g. 创建Part的扩展属性,则平台会在数据库中创建CF_PART表用于保存Part的扩展属性值。

7.2. 设值及持久化

为方便在代码中使用新增的字段,通常在代码中添加字段常量,类名的命名规则建议为对象名加上CEF ,例如:

public class UserCEF {
    public static final String USER_CF_USED_NAME = "USED_NAME";
    public static final String USER_CF_AGE = "AGE";
    public static final String USER_CF_BIRTHDAY = "BIRTHDAY";
}

设值使用setCEF()方法,调用对应service的save()方法保存:

user.setCEF(UserCEF.USER_CF_USED_NAME, "张三");
userService.save(user);

8. 自定义表

该章节主要介绍了使用自定义表实现一个对象的操作。区别于使用原生Hibernate实现对象的增删改查,该章节主要介绍对象类的定义及后台Service的编写,至于项目的结构及页面的编写可参考项目结构章节及实现一个对象的增删改查章节。

在只使用子对象做二次开发的情况项目结构与使用Hibernate的不同,因为使用自定义对象不需要写Dao及Handler层,所以不需要impl项目,具体可参考 119.119.119.4:9090/lumos/lumos-examples/tree/master/lumos-template-example 项目,该项目中有使用自定义表及扩展字段的例子。

8.1. 定义自定义表对象类

创建一个继承CustomizedObjectSupport的Bean对象:

package com.ags.lumosframework.doc.demo.customizedobject.domain;

import java.time.ZonedDateTime;

import com.ags.lumosframework.sdk.base.domain.CustomizedObjectSupport;
import com.ags.lumosframework.sdk.base.entity.IDynamicEntity;

public class Person extends CustomizedObjectSupport {

    public static final String TABLE_NAME = "PERSON";
    public static final String NAME = "NAME";
    public static final String AGE = "AGE";
    public static final String BIRTHDAY = "BIRTHDAY";

    public Person() {
        super(TABLE_NAME);
    }

    public Person(IDynamicEntity dynamicEntity) {
        super(dynamicEntity);
    }

    public String getPersonName() {
        return getAsString(NAME);
    }

    public void setPersonName(String name) {
        set(NAME, name);
    }

    public int getAge() {
        return getAsInt(AGE) == null ? 0 : getAsInt(AGE);
    }

    public void setAge(int age) {
        set(AGE, age);
    }

    public ZonedDateTime getBirthday() {
        return getAsZonedDateTime(BIRTHDAY);
    }

    public void setBirthday(ZonedDateTime birthday) {
        set(BIRTHDAY, birthday);
    }

}

其中NAME,AGE,BIRTHDAY是列名,必须和界面上添加的保持一致。

AddCustomizedTable
自定义表字段名不能是数据库的关键字,否则添加不成功。
创建自定义表平台会在数据库中创建“UT_”开头的表。

和扩展属性一样,自定义表字段也是有八种数据类型可供选择,其中Object类型字段会默认创建对应的外键引用,因此在设值时,需要注意置空:

public void setUser(User user) {
    if (user != null) {
        set(ASSIGN_USER, user.getId());
    } else {
        set(ASSIGN_USER, null);
    }
}

同时自定义对象基类还提供了大量通用的字段,当然,这些字段都不需要再界面上手动添加字段定义:

package com.ags.lumosframework.sdk.base.entity;

import java.time.ZonedDateTime;

public interface IBaseEntity extends ISetDataId, IGetData {

    void setCompanyId(long companyId);

    void setPlatformId(String platformId);

    void setCreateTime(ZonedDateTime createTime);

    void setCreateUserId(long createUserId);

    void setCreateUserName(String createUserName);

    void setCreateUserFullName(String createUserFullName);

    void setCreateIp(String createIp);

    /**
     * 内部使用
     *
     * @return
     */
    void setCreateBid(long creationBid);

    void setLastModifyTime(ZonedDateTime lastModifyTime);

    void setLastModifyUserId(long lastModifyUserId);

    void setLastModifyUserName(String lastModifyUserName);

    void setModifyBid(long modifyBid);

    void setRowLogId(String rowLogId);

    void setLastModifyUserFullName(String lastModifyUserFullName);

    void setLastModifyIp(String lastModifyIp);

    void setDeleteTime(ZonedDateTime deleteTime);

    void setDeleteUserId(long deleteUserId);

    void setDeleteUserName(String deleteUserName);

    void setDeleteIp(String deleteIp);

    void setDeleted(boolean deleted);

    void setDeleteUserFullName(String deleteUserFullName);
}

8.2. 定义自定义对象Service类

如果需要对数据库进行访问,需要写一个自定义对象的service接口及service的实现类,接口继承自ICustomizedObjectSupportService,实现类继承自AbstractCustomizedObjectSupportService。

IPersonService
package com.ags.lumosframework.doc.demo.customizedobject.service;

import com.ags.lumosframework.doc.demo.customizedobject.domain.Person;
import com.ags.lumosframework.sdk.base.service.api.ICustomizedObjectSupportService;

public interface IPersonService extends ICustomizedObjectSupportService<Person> {}
PersonService
package com.ags.lumosframework.doc.demo.customizedobject.service.impl;

import com.ags.lumosframework.sdk.base.service.AbstractCustomizedObjectSupportService;
import org.springframework.stereotype.Service;

import com.ags.lumosframework.doc.demo.customizedobject.domain.Person;
import com.ags.lumosframework.doc.demo.customizedobject.service.IPersonService;

@Service
public class PersonService extends AbstractCustomizedObjectSupportService<Person> implements IPersonService {

    @Override
    public String getTableName() {
        return Person.TABLE_NAME;
    }
}

平台提供的service基类已经提供了很多常用的方法:

package com.ags.lumosframework.sdk.base.service.api;

import com.ags.lumosframework.sdk.base.domain.ICustomizedObjectSupport;
import com.ags.lumosframework.sdk.base.filter.DynamicEntityFilter;

import java.util.List;

/**
 * 用于自定义表二次开发时自定义对象的service接口
 *
 * @param <T>
 */
public interface ICustomizedObjectSupportService<T extends ICustomizedObjectSupport> extends IBaseService {

    /**
     * 添加自定义对象
     *
     * @param obj
     * @return
     */
    T save(T obj);

    /**
     * 添加多个自定义对象
     *
     * @param objs
     */
    void saveAll(List<T> objs);

    /**
     * 根据id删除自定义对象
     *
     * @param objId
     */
    void deleteById(long objId);

    /**
     * 删除一个自定义对象
     *
     * @param obj
     */
    void delete(T obj);

    /**
     * 根据id查询一个自定义对象
     *
     * @param id
     * @return
     */
    T getById(long id);

    /**
     * 根据DynamicEntityFilter条件查询自定义对象
     *
     * @param entityFilter
     * @return
     */
    T getByFilter(DynamicEntityFilter entityFilter);

    /**
     * 查询所有自定义对象
     *
     * @return
     */
    List<T> list();

    /**
     * 根据条件查询自定义对象列表
     *
     * @param entityFilter
     * @return
     */
    List<T> listByFilter(DynamicEntityFilter entityFilter);

    /**
     * 查询数量
     *
     * @param entityFilter
     * @return
     */
    int countByFilter(DynamicEntityFilter entityFilter);

    String getTableName();
}

同时service基类还提供了一个通用的查询方法,使用DynamicEntityFilter指定多个查询条件进行查询。

9. 事务支持

9.1. 本地事务

本地事务表示同一个jvm内,代码调用链出现了调用如数据库等事务资源的代码,需要添加 spring的 @Transactional 注解支持本地事务

本地事务默认由spring直接支持,不需要额外的配置

本地事务只可以在handler和dao添加

9.2. 分布式事务

在使用rpc调用服务的情况下,需要使用分布式事务,用于将各个调用服务的本地事务变更为一整个分布式事务, 在需要使用的方法上添加 @TxTransaction 注解

分布式事务除了添加注解,还需要项目做些额外的配置才能支持,参照项目如何开启分布式事务支持

正常本地事务都需要支持分布式事务,所以方法上一般会同时存在 @Transactional @TxTransaction 两个注解, 如果方法只存在rpc调用,不存在本地事务资源调用,则只需要添加 @TxTransaction, 例如在使用自定义表做二次开发时,一般只写到service层,而service层不会出现 调用本地资源(数据库等)的代码,所以在service层可以只添加 @TxTransaction

9.2.1. 项目如何开启分布式事务支持

  • 从公司下载相关版本的txmanager并启动, 如需修改配置信息,参考如下

spring.application.name=TransactionManager
server.port=7970

# JDBC 数据库配置
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/tx-manager?characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=123456

# 数据库方言
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect

# 第一次运行可以设置为: create, 为TM创建持久化数据库表
spring.jpa.hibernate.ddl-auto=validate

# redis 的设置信息. 线上请用Redis Cluster
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=

# TM监听IP. 默认为 127.0.0.1
tx-lcn.manager.host=127.0.0.1

# TM监听Socket端口. 默认为 ${server.port} + 100
tx-lcn.manager.port=8070

# 心跳检测时间(ms). 默认为 300000
#tx-lcn.manager.heart-time=300000

# 分布式事务执行总时间(ms). 默认为5 * 60 * 1000
#tx-lcn.manager.dtx-time=300000

# 参数延迟删除时间单位ms  默认为dtx-time值
#tx-lcn.message.netty.attr-delay-time=${tx-lcn.manager.dtx-time}

# 事务处理并发等级. 默认为机器逻辑核心数5倍
#tx-lcn.manager.concurrent-level=160

# TM后台登陆密码,默认值为codingapi
#tx-lcn.manager.admin-key=codingapi

# 分布式事务锁超时时间 默认为-1,当-1时会用tx-lcn.manager.dtx-time的时间
#tx-lcn.manager.dtx-lock-time=${tx-lcn.manager.dtx-time}

# 雪花算法的sequence位长度,默认为12位.
#tx-lcn.manager.seq-len=12

# 异常回调开关。开启时请制定ex-url
#tx-lcn.manager.ex-url-enabled=false

# 事务异常通知(任何http协议地址。未指定协议时,为TM提供内置功能接口)。默认是邮件通知
#tx-lcn.manager.ex-url=/provider/email-to/***@**.com

# 开启日志,默认为false
#tx-lcn.logger.enabled=true
#tx-lcn.logger.enabled=false
#tx-lcn.logger.driver-class-name=${spring.datasource.driver-class-name}
#tx-lcn.logger.jdbc-url=${spring.datasource.url}
#tx-lcn.logger.username=${spring.datasource.username}
#tx-lcn.logger.password=${spring.datasource.password}
  • 在impl,sdk项目的pom文件里添加如下依赖

<dependency>
    <groupId>com.codingapi.txlcn</groupId>
    <artifactId>txlcn-tc</artifactId>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>com.codingapi.txlcn</groupId>
    <artifactId>txlcn-txmsg-netty</artifactId>
    <scope>provided</scope>
</dependency>
  • 在app启动项目的pom文件里添加如下依赖

<dependency>
    <groupId>com.codingapi.txlcn</groupId>
    <artifactId>txlcn-tc</artifactId>
</dependency>
<dependency>
    <groupId>com.codingapi.txlcn</groupId>
    <artifactId>txlcn-txmsg-netty</artifactId>
</dependency>
  • 在app启动项目配置文件添加如下

<dependency>
    <groupId>com.codingapi.txlcn</groupId>
    <artifactId>txlcn-tc</artifactId>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>com.codingapi.txlcn</groupId>
    <artifactId>txlcn-txmsg-netty</artifactId>
    <scope>provided</scope>
</dependency>
  • 在app启动项目的配置文件添加如下信息

# 是否启动LCN负载均衡策略(优化选项,开启与否,功能不受影响)
#tx-lcn.ribbon.loadbalancer.dtx.enabled=true

# tx-manager 的配置地址,可以指定TM集群中的任何一个或多个地址
# tx-manager 下集群策略,每个TC都会从始至终<断线重连>与TM集群保持集群大小个连接。
# TM方,每有TM进入集群,会找到所有TC并通知其与新TM建立连接。
# TC方,启动时按配置与集群建立连接,成功后,会再与集群协商,查询集群大小并保持与所有TM的连接
tx-lcn.client.manager-address=127.0.0.1:8070

# 该参数是分布式事务框架存储的业务切面信息。采用的是h2数据库。绝对路径。该参数默认的值为{user.dir}/.txlcn/{application.name}-{application.port}
#tx-lcn.aspect.log.file-path=logs/.txlcn/demo-8080

# 调用链长度等级,默认值为3(优化选项。系统中每个请求大致调用链平均长度,估算值。)
#tx-lcn.client.chain-level=3

# 该参数为事务方法注解切面的orderNumber,默认值为0.
#tx-lcn.client.dtx-aspect-order=0

# 该参数为事务连接资源方法切面的orderNumber,默认值为0.
#tx-lcn.client.resource-order=0

# 是否开启日志记录。当开启以后需要配置对应logger的数据库连接配置信息。
#tx-lcn.logger.enabled=false
#tx-lcn.logger.driver-class-name=${spring.datasource.driver-class-name}
#tx-lcn.logger.jdbc-url=${spring.datasource.url}
#tx-lcn.logger.username=${spring.datasource.username}
#tx-lcn.logger.password=${spring.datasource.password}
  • 在app启动项目的启动类上添加 @EnableDistributedTransaction

10. 缓存

平台默认对handler提供了缓存功能,默认对对象的id查询进行缓存

10.1. handler添加缓存

在handler接口上使用注解@CacheAdd,@CacheDelete

@CacheAdd表示需要添加一个缓存记录

@CacheDelete表示需要移除缓存记录

key表示缓存中的key,使用spring的SPEL实现,语法与SPEL相同

例如:

IBaseEntityHandler.class
public interface IBaseEntityHandler<T extends IBaseEntity> extends IBaseHandler {

    //key的实际值为id=????
    @CacheDelete(key = "'id=' + #entity.id")
    T save(T entity);

    void saveAll(List<T> entities);

    @CacheDelete(key = "'id=' + #t.id")
    void delete(T t);

    @CacheDelete(key = "'id=' + #entityId")
    void deleteById(long entityId);

    void deleteByIds(List<Long> entityIds);

    @CacheAdd(key = "'id=' + #id")
    T getById(long id);

10.2. CacheManager

平台在spring上下文中默认提供了一个CacheManager,handler默认提供了getCacheManage方法,使用该方法可以更灵活的对缓存进行操作, handler对所有可缓存对象的缓存都会有一个CacheUtils的String generateId(long id)方法生成的key,

CacheManager
package com.ags.lumosframework.common.cache;

public interface CacheManager {

    /**
     * 根据名称查询或者创建对应的缓存对象
     *
     * @param name
     * @return
     */
    Cache getOrCreateCache(String name);

    /**
     * 根据名称查询或者创建对应的缓存对象
     *
     * @param name
     * @param config
     *            创建缓存时使用的配置对象
     * @return
     */
    Cache getOrCreateCache(String name, Object config);

    /**
     * 清空缓存下的所有记录
     *
     * @param name
     */
    void clearCache(String name);

}

10.3. Cache

缓存对象,提供了缓存各种key-value更新查询

Cache
/*
 * Copyright 2002-2019 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */

package com.ags.lumosframework.common.cache;

import java.util.Optional;

public interface Cache {

    /**
     * 获取缓存的名字
     *
     * @return
     */
    String getName();

    Object getNativeCache();

    /**
     * 根据key获取缓存中的记录
     *
     * @param key
     * @return
     */
    Optional<Object> get(String key);

    /**
     * 返回对应类型的缓存记录
     *
     * @param key
     * @param type
     * @param <T>
     * @return
     */
    <T> Optional<T> get(String key, Class<T> type);

    /**
     * 添加一个缓存记录
     *
     * @param key
     * @param value
     * @param aliasNames
     *            缓存记录key的别名
     */
    void put(String key, Object value, String... aliasNames);

    /**
     * 先移除记录,再更新缓存
     *
     * @param key
     * @param value
     * @param aliasNames
     */
    void evictAndPut(String key, Object value, String... aliasNames);

    /**
     * 移除缓存记录
     *
     * @param key
     */
    void evict(String key);

    /**
     * 清空缓存
     */
    void clear();

}

11. Sequence

lumos 提供了序列号功能,用于支持自增流水号生成规则的定制

11.1. Sequence参数定义

用户可以添加一些参数定义,参数定义包含了参数的名字和参数的类型等信息, 该参数定义可以用于序列号类型表示序列号支持哪些参数, 也可用于格式化器表示格式化器所需的参数。

内置的提供的参数定义
package com.ags.lumosframework.common.parameter;

import com.ags.lumosframework.common.parameter.value.NumberRadixValues;
import com.ags.lumosframework.common.parameter.value.PadPositionValues;

public enum CommonParameterDefs implements IParameterDef {
    /**
     * 固定值
     */
    FIXED_VALUE("FIXED_VALUE", "FIXED_VALUE", "parameter.def.fixed-value", ParameterType.String),
    /**
     * 自定义值
     */
    CUSTOMIZED_VALUE("CUSTOMIZED_VALUE", "CUSTOMIZED_VALUE", "parameter.def.customized-value", ParameterType.String),

    /**
     * 流水号(一般表示自增的数值类型)
     */
    SERIAL_NUMBER("SERIAL_NUMBER", "SERIAL_NUMBER", "parameter.def.serial-number", ParameterType.Number),
    /**
     * 日
     */
    DAY("DAY", "DAY", "parameter.def.day", ParameterType.Number),
    /**
     * 月
     */
    MONTH("MONTH", "MONTH", "parameter.def.month", ParameterType.Number),
    /**
     * 年
     */
    YEAR("YEAR", "YEAR", "parameter.def.year", ParameterType.Number),

    /**
     * 原始进制,(格式化器使用的参数)
     */
    ORIGIN_RADIX("ORIGIN_RADIX", "ORIGIN_RADIX", "parameter.def.origin-radix", ParameterType.ComplexSingleSelect,
        new IParameterValue[] {NumberRadixValues.RADIX10, NumberRadixValues.RADIX16, NumberRadixValues.RADIX34,
            NumberRadixValues.RADIX36},
        NumberRadixValues.RADIX10),

    /**
     * 目标进制,(格式化器使用的参数)
     */
    TARGET_RADIX("TARGET_RADIX", "TARGET_RADIX", "parameter.def.target-radix", ParameterType.ComplexSingleSelect,
        new IParameterValue[] {NumberRadixValues.RADIX10, NumberRadixValues.RADIX16, NumberRadixValues.RADIX34,
            NumberRadixValues.RADIX36}),
    /**
     * 左补齐还是又补齐,(格式化器使用的参数)
     */
    PAD_POSITION("PAD_POSITION", "PAD_POSITION", "parameter.def.pad-position", ParameterType.ComplexSingleSelect,
        new IParameterValue[] {PadPositionValues.LEFT, PadPositionValues.RIGHT}, PadPositionValues.LEFT),
    /**
     * 补齐长度,(格式化器使用的参数)
     */
    PAD_LENGTH("PAD_LENGTH", "PAD_LENGTH", "parameter.def.pad-length", ParameterType.Number),
    /**
     * 补齐的占位符,(格式化器使用的参数)
     */
    PAD_PLACE_HOLDER("PAD_PLACE_HOLDER", "PAD_PLACE_HOLDER", "parameter.def.pad-place-holder", ParameterType.Char),;

    String key;
    String name;
    String nameI18NKey;
    ParameterType parameterType;
    IParameterValue[] parameterValues;
    IParameterValue defaultParameterValue;

    CommonParameterDefs(String key, String name, String nameI18NKey, ParameterType parameterType) {
        this.key = key;
        this.name = name;
        this.nameI18NKey = nameI18NKey;
        this.parameterType = parameterType;
    }

    CommonParameterDefs(String key, String name, String nameI18NKey, ParameterType parameterType,
        IParameterValue[] parameterValues) {
        this(key, name, nameI18NKey, parameterType);
        this.parameterValues = parameterValues;
    }

    CommonParameterDefs(String key, String name, String nameI18NKey, ParameterType parameterType,
        IParameterValue[] parameterValues, IParameterValue defaultParameterValue) {
        this(key, name, nameI18NKey, parameterType);
        this.parameterValues = parameterValues;
        this.defaultParameterValue = defaultParameterValue;
    }

    @Override
    public String getKey() {
        return key;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getNameI18NKey() {
        return nameI18NKey;
    }

    @Override
    public ParameterType getType() {
        return parameterType;
    }

    @Override
    public IParameterValue[] getParameterValues() {
        return parameterValues;
    }

    @Override
    public IParameterValue getDefaultParameterValue() {
        return defaultParameterValue;
    }
}

在添加sequence参数时,列表信息来源于sequence类型支持的参数信息

在对参数信息添加格式化器时,每个格式化器相关的控件配置信息来源于格式化器支持的参数信息

11.1.1. 实现一个IParameterDef

IParameterDef接口定义
package com.ags.lumosframework.common.parameter;

public interface IParameterDef {

    /**
     * parameter 的key
     *
     * @return
     */
    String getKey();

    /**
     * parameter 的name
     *
     * @return
     */
    String getName();

    /**
     * name 国际化key
     *
     * @return
     */
    String getNameI18NKey();

    ParameterType getType();

    default IParameterValue[] getParameterValues() {
        return null;
    };

    default IParameterValue getDefaultParameterValue() {
        return null;
    }

}
DocParameterDefs
package com.ags.lumosframework.doc.demo;

import com.ags.lumosframework.common.parameter.IParameterDef;
import com.ags.lumosframework.common.parameter.IParameterValue;
import com.ags.lumosframework.common.parameter.ParameterType;

public enum DocParameterDefs implements IParameterDef {
    /**
     *
     */
    DOC_NUMBER("ORDER_NUMBER", "ORDER_NUMBER", "parameter.def.doc-number", ParameterType.String);

    String key;
    String name;
    String nameI18NKey;
    ParameterType parameterType;
    IParameterValue[] parameterValues;
    IParameterValue defaultParameterValue;

    DocParameterDefs(String key, String name, String nameI18NKey, ParameterType parameterType) {
        this.key = key;
        this.name = name;
        this.nameI18NKey = nameI18NKey;
        this.parameterType = parameterType;
    }

    DocParameterDefs(String key, String name, String nameI18NKey, ParameterType parameterType,
        IParameterValue[] parameterValues) {
        this(key, name, nameI18NKey, parameterType);
        this.parameterValues = parameterValues;
    }

    DocParameterDefs(String key, String name, String nameI18NKey, ParameterType parameterType,
        IParameterValue[] parameterValues, IParameterValue defaultParameterValue) {
        this(key, name, nameI18NKey, parameterType);
        this.parameterValues = parameterValues;
        this.defaultParameterValue = defaultParameterValue;
    }

    @Override
    public String getKey() {
        return key;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getNameI18NKey() {
        return nameI18NKey;
    }

    @Override
    public ParameterType getType() {
        return parameterType;
    }

    @Override
    public IParameterValue[] getParameterValues() {
        return parameterValues;
    }

    @Override
    public IParameterValue getDefaultParameterValue() {
        return defaultParameterValue;
    }
}

11.1.2. 在META-INF/spring.factories添加如下语句

DocParameterDefs
com.ags.lumosframework.common.parameter.IParameterDef=\
com.ags.lumosframework.doc.demo.DocParameterDefs

11.2. Sequence扩展类型添加

添加扩展的sequence类型,在sequence页面新增sequence时可以选择扩展的类型

ISequenceType接口定义
package com.ags.lumosframework.sdk.sequence;

import java.util.ArrayList;
import java.util.List;

import com.ags.lumosframework.common.parameter.CommonParameterDefs;
import com.ags.lumosframework.common.parameter.IParameterDef;

public interface ISequenceType {

    /**
     * sequence名字
     *
     * @return
     */
    String getName();

    /**
     * sequence名字国际化key
     *
     * @return
     */
    String getNameI18NKey();

    List<IParameterDef> getParameterDefs();

    /**
     * 是否支持扩展属性
     *
     * @return
     */
    boolean isSupportCustomizedParam();

    /**
     * 默认提供的参数列表
     *
     * @return
     */
    default List<IParameterDef> getDefaultParameterDefs() {
        List<IParameterDef> parameterDefList = new ArrayList<>();
        parameterDefList.add(CommonParameterDefs.YEAR);
        parameterDefList.add(CommonParameterDefs.MONTH);
        parameterDefList.add(CommonParameterDefs.DAY);
        parameterDefList.add(CommonParameterDefs.SERIAL_NUMBER);
        parameterDefList.add(CommonParameterDefs.FIXED_VALUE);
        return parameterDefList;
    }
}

参数定义参考Sequence参数定义

11.2.1. 实现一个ISequenceType

DocParameterDefs
package com.ags.lumosframework.doc.demo.sequence;

import java.util.ArrayList;
import java.util.List;

import com.ags.lumosframework.common.parameter.IParameterDef;
import com.ags.lumosframework.doc.demo.DocParameterDefs;
import com.ags.lumosframework.sdk.sequence.ISequenceType;

public class DocSequenceType implements ISequenceType {

    public static String NAME = "Doc";

    @Override
    public String getName() {
        return NAME;
    }

    @Override
    public String getNameI18NKey() {
        return "admin.doc";
    }

    @Override
    public List<IParameterDef> getParameterDefs() {
        List<IParameterDef> parameterDefList = new ArrayList<>(getDefaultParameterDefs());
        parameterDefList.add(DocParameterDefs.DOC_NUMBER);
        return parameterDefList;
    }

    @Override
    public boolean isSupportCustomizedParam() {
        return false;
    }
}

11.2.2. 在META-INF/spring.factories添加如下语句

DocSequenceType
com.ags.lumosframework.sdk.sequence.ISequenceType=\
com.ags.lumosframework.doc.demo.sequence.DocSequenceType

11.3. Sequence格式化器定义

用户可以定义自己的格式化器用于支持对相关的参数进行转换。

lumos 内置提供了NumberRadixFormatter和LeftRightPadFormatter两种格式化器,用于对sequence的参数进行格式化转换

NumberRadixFormatter用于对数值类型的参数进行进制的转换

LeftRightPadFormatter用于对参数进行左边或右边长度填充,例如对1使用字符‘0’进行左边长度为8的填充(000000001)

11.3.1. 实现一个IFormatter

IFormatter接口定义
package com.ags.lumosframework.common.formatter;

import com.ags.lumosframework.common.parameter.IParameterDef;

/**
 * 格式化接口
 */
public interface IFormatter {

    /**
     * 对原始文本进行格式化
     *
     * @param text
     *            原始文本
     * @param params
     *            格式化所需的参数
     * @return
     */
    String format(String text, FormatterConfig formatterConfig);

    /**
     * formatter 的id
     *
     * @return
     */
    String getId();

    /**
     * formatter 的name
     *
     * @return
     */
    String getName();

    /**
     * formatter 的name i18n key
     *
     * @return
     */
    String getNameI18NKey();

    IParameterDef[] getParameterDefs();
}

参数定义参考Sequence参数定义

内置LeftRightPadFormatter实现
package com.ags.lumosframework.common.formatter;

import java.util.HashMap;
import java.util.Map;

import org.apache.commons.lang3.StringUtils;

import com.ags.lumosframework.common.parameter.CommonParameterDefs;
import com.ags.lumosframework.common.parameter.IParameterDef;
import com.ags.lumosframework.common.parameter.value.PadPositionValues;

public class LeftRightPadFormatter extends AbstractFormatter {

    public static String ID = "LEFT_RIGHT_PAD_FORMATTER";

    public static void main(String[] args) {

        Map<String, Object> stringObjectMap = new HashMap<>();
        stringObjectMap.put(CommonParameterDefs.PAD_POSITION.getKey(), PadPositionValues.RIGHT.getValue());
        stringObjectMap.put(CommonParameterDefs.PAD_PLACE_HOLDER.getKey(), "A");
        stringObjectMap.put(CommonParameterDefs.PAD_LENGTH.getKey(), 10);
        FormatterConfig formatterConfig = new FormatterConfig();
        formatterConfig.addConfigParams(stringObjectMap);

        IFormatter numberRadixFormatter = Formatters.getById(LeftRightPadFormatter.ID);
        String format = numberRadixFormatter.format("123578", formatterConfig);
        System.err.println(format);
    }

    @Override
    public String getId() {
        return ID;
    }

    @Override
    public String getName() {
        return "LeftRightPadFormatter";
    }

    @Override
    public String getNameI18NKey() {
        return "formatters.left-right-pad-formatter";
    }

    @Override
    public IParameterDef[] getParameterDefs() {
        return new IParameterDef[] {CommonParameterDefs.PAD_POSITION, CommonParameterDefs.PAD_LENGTH,
            CommonParameterDefs.PAD_PLACE_HOLDER};
    }

    @Override
    protected String processFormat(String text, FormatterConfig formatterConfig) {
        String padPosition = (String)formatterConfig.getConfigParams().get(CommonParameterDefs.PAD_POSITION.getKey());
        String padPlaceHolder =
            (String)formatterConfig.getConfigParams().get(CommonParameterDefs.PAD_PLACE_HOLDER.getKey());
        Number padLength = (Number)formatterConfig.getConfigParams().get(CommonParameterDefs.PAD_LENGTH.getKey());
        if (PadPositionValues.LEFT.getValue().equals(padPosition)) {
            return StringUtils.leftPad(text, padLength.intValue(), padPlaceHolder);
        } else {
            return StringUtils.rightPad(text, padLength.intValue(), padPlaceHolder);
        }
    }
}
内置NumberRadixFormatter 实现
package com.ags.lumosframework.common.formatter;

import java.util.HashMap;
import java.util.Map;

import com.ags.lumosframework.common.parameter.CommonParameterDefs;
import com.ags.lumosframework.common.parameter.IParameterDef;
import com.ags.lumosframework.common.parameter.value.NumberRadixValues;
import com.ags.lumosframework.common.spring.BeanManager;
import com.ags.lumosframework.common.util.NumberRadixUtils;

public class NumberRadixFormatter extends AbstractFormatter {

    public static String ID = "NUMBER_RADIX_FORMATTER";

    public static void main(String[] args) {
        Map<String, Object> stringObjectMap = new HashMap<>();
        stringObjectMap.put(CommonParameterDefs.TARGET_RADIX.getKey(), 36);
        FormatterConfig formatterConfig = new FormatterConfig();
        formatterConfig.addConfigParams(stringObjectMap);

        IFormatter numberRadixFormatter = Formatters.getById(NumberRadixFormatter.ID);
        String format = numberRadixFormatter.format("123578", formatterConfig);
        System.err.println(format);

    }

    @Override
    public String getId() {
        return ID;
    }

    @Override
    public String getName() {
        return "NumberRadixFormatter";
    }

    @Override
    public String getNameI18NKey() {
        return "formatters.number-radix-formatter";
    }

    @Override
    public IParameterDef[] getParameterDefs() {
        return new IParameterDef[] {CommonParameterDefs.ORIGIN_RADIX, CommonParameterDefs.TARGET_RADIX};
    }

    @Override
    protected String processFormat(String text, FormatterConfig formatterConfig) {
        Integer originRadix = BeanManager.getConversionService()
            .convert(formatterConfig.getConfigParams().get(CommonParameterDefs.ORIGIN_RADIX.getKey()), Integer.class);
        Integer targetRadix = BeanManager.getConversionService()
            .convert(formatterConfig.getConfigParams().get(CommonParameterDefs.TARGET_RADIX.getKey()), Integer.class);
        long originalNumber = Long.parseLong(text, originRadix);
        if (NumberRadixValues.RADIX34.getValue().equals(targetRadix)) {
            return NumberRadixUtils.to34(originalNumber);
        } else {
            return Long.toString(originalNumber, targetRadix);
        }
    }
}

11.3.2. 在META-INF/spring.factories添加如下语句

DocSequenceType
com.ags.lumosframework.common.formatter.IFormatter=\
com.ags.lumosframework.common.formatter.NumberRadixFormatter,\
com.ags.lumosframework.common.formatter.LeftRightPadFormatter

11.4. 添加一个序列号定义

在管理项页面添加一个序列号定义,选择内置的或者扩展的序列号类型

SequenceAdd

11.5. 添加一个序列号参数

选中添加的序列号,添加序列号对应的参数

类型

参数类型,一般在生成序列号时需要传入该参数的key

重置

用于表示在生成序列号时,该参数的改变是否会重置序列号初始值,比如年月日参数重置后,每日都会从0开始

排序序号

表示参数的排序序号,生成序列号时会按照序号顺序拼装参数

进制转换

用于对参数进制进制的装换, 原始进制表示参数值的原始进制数,目标进制表示期望转换的进制数

两端补齐

用于对参数值在长度不够的情况下补齐, 补齐位置表示是左边补齐还是右边补齐, 长度表示参数值最终的总长度, 占位符表示用什么字符去补齐

参数创建后,编辑时不可以更改参数类型和重置字段

SequenceParamAdd

11.6. 生成序列号

ISequenceDefHandler接口提供了接口用于生成对应的序列号,在各自的service类注入该类直接使用

ISequenceDefHandler接口定义
package com.ags.lumosframework.sdk.handler.api;

import com.ags.lumosframework.common.cache.annotation.CacheAdd;
import com.ags.lumosframework.sdk.base.handler.api.IBaseEntityHandler;
import com.ags.lumosframework.sdk.dubbo.RpcSupport;
import com.ags.lumosframework.sdk.entity.SequenceDefEntity;

import java.util.List;
import java.util.Map;

@RpcSupport
public interface ISequenceDefHandler extends IBaseEntityHandler<SequenceDefEntity> {

    /**
     * @param sequenceDef
     *            上文添加的序列号定义
     * @param params
     *            参数值,与上文添加的参数定义对应,部分参数字段可以不需要传入(年月日,序列号)
     * @return 返回生成的序列号
     */
    String next(SequenceDefEntity sequenceDef, Map<String, Object> params);

    /**
     * @param sequenceDef
     *            上文添加的序列号定义
     * @param params
     *            参数值,与上文添加的参数定义对应,部分参数字段可以不需要传入(年月日,序列号)
     * @param count
     *            生成序列号的数目
     * @return 返回生成的序列号列表
     */
    List<String> next(SequenceDefEntity sequenceDef, Map<String, Object> params, int count);

    /**
     * @param sequenceDef
     *            上文添加的序列号定义
     * @param params
     *            参数值,可以只包含参数定义中勾选了重置的参数,因为只查询,不生成
     * @return 返回当前的值
     */
    long current(SequenceDefEntity sequenceDef, Map<String, Object> params);

    @CacheAdd(key = "'sequenceName=' + #sequenceName")
    SequenceDefEntity getByName(String sequenceName);

    /**
     * 根据sequenceType查询对应的序列号定义,多个的情况下只返回
     *
     * @param sequenceType
     * @return
     */
    SequenceDefEntity getDefaultForSequenceType(String sequenceType);

    /**
     * 获取所有的有效序列号定义
     *
     * @return 序列号定义集合
     */
    List<SequenceDefEntity> listSequenceDefs();

    /**
     * 根据序列号类型获取有效的序列号定义集合
     * @param sequenceType
     * @return
     */
    List<SequenceDefEntity> listSequenceDefsBySequenceType(String sequenceType);
}

11.7. 内置的序列号绑定组件

11.7.1. 实现IAssignSequence接口

IAssignSequence接口定义
package com.ags.lumosframework.web.vaadin.ui.admin.view.sequence.assign;

import com.ags.lumosframework.sdk.domain.SequenceDef;

import java.util.List;
import java.util.Set;

public interface IAssignSequence<T> {

    /**
     * 查询绑定对象的序列号定义
     *
     * @param t
     *            绑定的对象
     * @return
     */
    List<SequenceDef> listAssignedSequenceDefs(T t);

    /**
     * 给对象绑定序列号定义id列表
     *
     * @param t
     * @param sequenceDefIds
     *            序列号定义id列表
     */
    void assignSequenceDefs(T t, Set<Long> sequenceDefIds);

    /**
     * 获取所有的 有效序列号定义
     *
     * @return 有效序列号定义集合
     */
    List<SequenceDef> listSequenceDefs();

}

11.7.2. 在需要绑定sequence对象的页面添加SequenceDefTab

将SequenceDefTab添加到任意页面即可

ISequenceDefHandler接口定义
//构造函数的参数为IAssignSequence接口的实现
SequenceDefTab sequenceDefTab = new SequenceDefTab<>(assignSequence);

12. 数据字典

平台具有数据字典功能,运行其他应用以及项目直接引用,对数据字段进行扩展。 数据字典的使用场景,主要在于对数据类型的扩展,可以省去很多基础数据表的增删改查。

比如场景1,在实现某项目中,需要对客户的产品不良提供分类,如果使用传统方法,则需要创建不良分类的基础表,然后提供增删改查页面,比较麻烦。
如果借助于数据字典,该不良分类仅仅是字典里面的一条记录,然后在该记录下以键值对的形式提供扩展,实施者可以收集这些值放入Combobox等控件中。
在二次开发中比较好用。
..\images\core\data dictionary
具体调用的service接口类:IDataDictionaryService ,实现类:DataDictionaryService。

13. 扩展系统菜单

Lumos脚手架中,默认提供一个应用:管理项。 其他的应用可以通过扩展的方式加入系统。

13.1. 扩展系统应用

如果要扩展系统的应用,只需要借助于如下一个注解即可。

系统在启动时,会自动加载所有被该注解标识的UI,自动解析出应用信息放入系统缓存备用
package com.ags.lumosframework.web.vaadin.base.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface WebEntry {

    /**
     * 应用所属的应用分组,如管理相关,运行期相关等,可以为空
     *
     * @return
     */
    String groupName() default "";

    /**
     * 在首页显示时使用的icon路径,该路径可以为网络路径,也可以是项目中的相对路径
     *
     * @return
     */
    String iconPath() default "";

    /**
     * 在首页显示时使用的icon路径,该路径可以为网络路径,也可以是项目中的相对路径
     *
     * @return
     */
    String[] backgroundPath() default {"platform/img/index/bg1.png", "platform/img/index/bg2.png"};

    /**
     * 在首页显示时的css样式信息
     *
     * @return
     */
    String cssClass() default "";

    /**
     * 首页显示该应用时的标题信息
     *
     * @return
     */
    String shortCaption() default "";

    /**
     * 首页显示时标题的国际化信息
     *
     * @return
     */
    String shortCaptionI18NKey() default "";

    /**
     * 首页显示该应用的时候,可以显示一段描述
     *
     * @return
     */
    String longCaption() default "";

    /**
     * 首页显示该应用的时候,可以显示一段描述的国际化信息
     *
     * @return
     */
    String longCaptionI18NKey() default "";

    /**
     * 首页显示该应用的时候,可以显示一段描述
     *
     * @return
     */
    String description() default "";

    /**
     * 首页显示该应用的时候,可以显示一段描述的国际化信息
     *
     * @return
     */
    String descriptionI18NKey() default "";

    /**
     * 显示在首页的排序,数值越小,优先级越高,反之亦然
     *
     * @return
     */
    int order() default 0;

    /**
     * 如果该应用在进入页面后有菜单,并且菜单有分组信息,那么可以预先加载分组信息.
     * <p>
     * 这个分组信息也可以不在此时定义,后续依然可以通过扩展接口的方式实现
     *
     * @return
     */
    MenuGroup[] viewGroups() default {};
}
MenuGroup定义
package com.ags.lumosframework.web.vaadin.base.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
public @interface MenuGroup {

    /**
     * 应用侧边栏分组信息的名称
     *
     * @return
     */
    String name();

    /**
     * 应用侧边栏的标题
     *
     * @return
     */
    String caption();

    /**
     * 应用侧边栏的标题的国际化信息
     *
     * @return
     */
    String captionI18NKey() default "";

    /**
     * 如果定义icon,那么该icon将显示在菜单分组信息的左侧
     *
     * @return
     */
    String iconPath();

    /**
     * 该菜单分组的排序,数值越小,越靠前
     *
     * @return
     */
    int order();
}

如下是一个简单的样例实现,供参考:

package com.ags.lumosframework.web.vaadin.ui.admin;

import com.ags.lumosframework.web.common.i18.I18NUtility;
import com.ags.lumosframework.web.common.security.annotation.Secured;
import com.ags.lumosframework.web.vaadin.base.BaseUIHasMenu;
import com.ags.lumosframework.web.vaadin.base.annotation.MenuGroup;
import com.ags.lumosframework.web.vaadin.base.annotation.WebEntry;
import com.ags.lumosframework.web.vaadin.constants.LumosConstants;
import com.ags.lumosframework.web.vaadin.constants.LumosPermissionConstants;
import com.vaadin.annotations.Theme;
import com.vaadin.server.Page;
import com.vaadin.spring.annotation.SpringUI;

@WebEntry(longCaption = "管理项", shortCaption = "Administration", shortCaptionI18NKey = "ui.administration",
    description = "For enterprise IT managers to perform system level management.",
    descriptionI18NKey = "Home.Page.Administration.Description", iconPath = "platform/img/index/icon_Administration.png",
    backgroundPath = {"platform/img/index/Administration1.png", "platform/img/index/Administration2.png"}, order = 400,
    viewGroups = {@MenuGroup(name = LumosConstants.ADMIN_OC_RELATED_Default, // oc相关
        captionI18NKey = LumosConstants.ADMIN_OC_RELATED_I18N, caption = LumosConstants.ADMIN_OC_RELATED_Default,
        iconPath = "", order = 0),
        @MenuGroup(name = LumosConstants.ADMIN_ADVANCED_SETTING_Default, // 高级设定
            captionI18NKey = LumosConstants.ADMIN_ADVANCED_SETTING_I18N,
            caption = LumosConstants.ADMIN_ADVANCED_SETTING_Default, iconPath = "", order = 1)})
@SpringUI(path = "administration")
@Secured(LumosPermissionConstants.ADMINISTRATION_MANAGE)
@Theme("light")
public class AdminUI extends BaseUIHasMenu {

    /**
     *
     */
    private static final long serialVersionUID = -8225969815229992750L;

    @Override
    protected void setTitle() {
        Page.getCurrent().setTitle(I18NUtility.getValue("ui.administration", "Administration"));
    }

}

13.2. 扩展或添加系统应用页面

实施人员可以很方便向既有的应用中添加一个页面,只需要实现如下的注解即可。

系统在启动时,会遍历系统中所有该注解标注的页面,放入系统缓存,等用户访问页面时,将根据这些页面信息,自动生成相应的菜单,在菜单中提供了通向这些页面的链接。
package com.ags.lumosframework.web.vaadin.base.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface Menu {

    /**
     * 如果指定,将在把应用放在菜单时,自动寻找菜单分组放入
     *
     * @return
     */
    String groupName() default "";

    /**
     * 应用在菜单上显示的名称
     *
     * @return
     */
    String caption();

    /**
     * 应用在菜单上显示的名称的国际化信息
     *
     * @return
     */
    String captionI18NKey() default "";

    /**
     * 如果定义,将在菜单左侧显示icon
     *
     * @return
     */
    String iconPath();

    /**
     * 应用在菜单分组时的排序信息
     *
     * @return
     */
    int order();
}

如下是系统中用户界面的简单实现,供大家参考:

@Menu(groupName = LumosConstants.ADMIN_OC_RELATED_Default, caption = "user", captionI18NKey = "admin.user",
    iconPath = "images/icon/user.png", order = 0)
@SpringView(name = "user", ui = AdminUI.class)
@Secured(LumosPermissionConstants.USER_MANEGE)
public class UserView extends BaseView implements Button.ClickListener, IFilterableView {

    /**
     *
     */
    private static final long serialVersionUID = -6316701858905840322L;

    @Secured(LumosPermissionConstants.USER_ADD)
    @I18Support(caption = "Add", captionKey = "common.add")
    private Button btnAdd = new Button();

    @Secured(LumosPermissionConstants.USER_EDIT)
    @I18Support(caption = "Edit", captionKey = "common.edit")
    private Button btnEdit = new Button();

    @Secured(LumosPermissionConstants.USER_RESET_PASSWORD)
    @I18Support(caption = "Reset Password", captionKey = "admin.user.resetpassword")
    private Button btnResetPassword = new Button();

13.3. UI中加入自定义的菜单

实施人员可以添加自己定义的菜单,然后在指定的UI中显示

例如:我们在管理项中创建一个新的报表

report

如图所示我们在报表页面可以看到新创建的报表,它根据创建的category,显示在对应的Group下

report menu

具体实现代码如下

package com.ags.lumosframework.web.vaadin.ui.report;

import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;

import com.ags.lumosframework.web.vaadin.base.CoreSideBar;
import com.ags.lumosframework.web.vaadin.constants.LumosConstants;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.vaadin.sliderpanel.SliderPanel;

import com.ags.lumosframework.common.constant.CommonConstants;
import com.ags.lumosframework.common.spring.BeanManager;
import com.ags.lumosframework.sdk.domain.I18NText;
import com.ags.lumosframework.sdk.domain.Permission;
import com.ags.lumosframework.sdk.domain.Report;
import com.ags.lumosframework.sdk.service.api.IReportService;
import com.ags.lumosframework.web.common.functionality.ApplicationInfo;
import com.ags.lumosframework.web.common.functionality.ApplicationManager;
import com.ags.lumosframework.web.common.functionality.ApplicationPageGroup;
import com.ags.lumosframework.web.common.functionality.ApplicationPageInfo;
import com.ags.lumosframework.web.common.i18.I18NUtility;
import com.ags.lumosframework.web.common.security.annotation.Secured;
import com.ags.lumosframework.web.common.security.annotation.SecuredInfo;
import com.ags.lumosframework.web.vaadin.base.BaseUIHasMenu;
import com.ags.lumosframework.web.vaadin.base.annotation.WebEntry;
import com.ags.lumosframework.web.vaadin.constants.LumosPermissionConstants;
import com.ags.lumosframework.web.vaadin.ui.report.customreport.IBirtReportView;
import com.vaadin.annotations.Theme;
import com.vaadin.server.Page;
import com.vaadin.server.VaadinRequest;
import com.vaadin.spring.annotation.SpringUI;

@WebEntry(longCaption = "报表", shortCaption = "Report", shortCaptionI18NKey = "Common.Report",
    description = "Visual Display and Individual Formulation of Data Statistical Statements.",
    descriptionI18NKey = "Home.Page.Report.Description", iconPath = "platform/img/index//icon_Report.png",
    backgroundPath = {"platform/img/index/Report1.png", "platform/img/index/Report2.png"}, order = 500)
@SpringUI(path = CommonConstants.REPORT)
@Secured(LumosPermissionConstants.REPORT_MANAGE1)
@Theme("light")
public class ReportUI extends BaseUIHasMenu {

    private static final long serialVersionUID = -552520960746758358L;

    protected SliderPanel sliderPanel;

    private SortedMap<ApplicationPageGroup, SortedSet<ApplicationPageInfo>> defaultMenuGroups = new TreeMap<>();

    @Override
    protected void setTitle() {
        Page.getCurrent().setTitle(I18NUtility.getValue("Common.Report", "Report"));
    }

    @Override
    protected void initSideBar() {

    }

    @Override
    protected void init(VaadinRequest request) {
        super.init(request);

        // bgtaskListener.putConsumer(MesConstants.REPORT +
        // Thread.currentThread().getId(), header);
        // sliderPanel = bgTaskPanel.getSliderPanel();
        // header.setBackgroundTaskBtnCallback(() -> {
        // if (sliderPanel != null) {
        // sliderPanel.expand();
        // }
        // });
        // mainCountent.addComponent(sliderPanel);

        addExtendMenuGroup();
    }

    private void addExtendMenuGroup() {
        ApplicationInfo appInfoByAppPath = ApplicationManager.getAppInfoByAppPath(CommonConstants.REPORT);
        SortedSet<ApplicationPageGroup> groups = ApplicationManager.getAppPageGroupsByApp(appInfoByAppPath);
        for (ApplicationPageGroup group : groups) {
            SortedSet<ApplicationPageInfo> pageInfos = ApplicationManager.getAppPageInfosByGroup(group);
            defaultMenuGroups.put(group, pageInfos);
        }

        // 用户自定义
        List<Report> list = BeanManager.getService(IReportService.class).list(0, Integer.MAX_VALUE);
        Collections.sort(list, new Comparator<Report>() {

            @Override
            public int compare(Report arg0, Report arg1) {
                return arg0.getOrder() - arg1.getOrder();
            }
        });

        // customizedMenuGroups.addAll(ApplicationManager.getAppPageGroupsByApp(appInfoByAppPath));
        int flag = 4;
        for (Report report : list) {
            String menuName = getMenuName(report);
            if (findDefaultPageGroup(menuName) == null) {
                String captionI18N = report.getCategoryI18N() == null ? "" : report.getCategoryI18N().getName();
                ApplicationPageGroup build =
                    ApplicationPageGroup.builder().applicationInfo(appInfoByAppPath).name(menuName)
                        .caption(report.getCategory()).captionI18NKey(captionI18N).order(++flag).active(false).build();
                defaultMenuGroups.put(build, new TreeSet<ApplicationPageInfo>());
            }
        }

        for (Report report : list) {
            ApplicationPageGroup apg1 = findDefaultPageGroup(getMenuName(report));
            if (apg1 != null) {
                SecuredInfo si = new SecuredInfo();
                Permission privilege = report.getPrivilege();
                si.setRequiredPermission(privilege == null ? "" : privilege.getName());
                I18NText menuCaptionI18N = report.getMenuCaptionI18N();
                String menuCaptionI18NId =
                    menuCaptionI18N == null ? report.getMenuCaption() : menuCaptionI18N.getName();
                ApplicationPageInfo pageInfo = ApplicationPageInfo.builder().caption(report.getMenuCaption())
                    .captionI18NKey(menuCaptionI18NId).applicationInfo(appInfoByAppPath).group(apg1)
                    .name(IBirtReportView.NAME + "/i18n=" + menuCaptionI18NId + ":" + report.getMenuCaption() + "&"
                        + IBirtReportView.REPORT_URL_KEY + "=" + report.getId())
                    .order(report.getOrder()).securedInfo(si).build();
                defaultMenuGroups.get(apg1).add(pageInfo);
            }
        }
        addPageGroup(defaultMenuGroups);
    }

    private String getMenuName(Report report) {
        I18NText text = report.getCategoryI18N();
        if (text != null) {
            if (LumosConstants.MENU_GROUP_PRODUCTION_KANBAN_DEFAULT_ZH.equals(text.getName())) {
                return LumosConstants.MENU_GROUP_PRODUCTION_KANBAN_DEFAULT;
            } else if (LumosConstants.MENU_GROUP_OUTPUT_DEFAULT_ZH.equals(text.getName())) {
                return LumosConstants.MENU_GROUP_OUTPUT_DEFAULT;
            } else if (LumosConstants.MENU_GROUP_WIP_DEFAULT_ZH.equals(text.getName())) {
                return LumosConstants.MENU_GROUP_WIP_DEFAULT;
            } else if (LumosConstants.MENU_GROUP_WORKORDER_DEFAULT_ZH.equals(text.getName())) {
                return LumosConstants.MENU_GROUP_WORKORDER_DEFAULT;
            } else if (LumosConstants.MENU_GROUP_QUALITY_DEFAULT_ZH.equals(text.getName())) {
                return LumosConstants.MENU_GROUP_QUALITY_DEFAULT;
            } else {
                return report.getCategoryI18NId() + "";
            }
        } else {
            return report.getCategory();
        }

    }

    private ApplicationPageGroup findDefaultPageGroup(String name) {
        for (ApplicationPageGroup apg : defaultMenuGroups.keySet()) {
            if (apg.getName().contentEquals(name)) {
                return apg;
            }
        }
        return null;
    }

}

13.4. 自定义TabSheet显示Caption

在上述实现方法通过将自定义的页面信息封装成ApplicationPageGroup 对象添加到 ApplicationInfo中; 系统在启动时 ,自动生成相应的菜单,选 中一个View时,会调用相关方法,将view添加到Tab中,展示该caption。

如果我们要自定义一个TabSheet, 就需要在ViewChangeEvent事件对象添加相关参数。

例如WMS中选中工单查看,对应参数的添加
UI.getCurrent().getNavigator().navigateTo(IOrderDetailView.NAME + "/" + IOrderDetailView.ORDER_NO
       + "=" + orderHeader.getOrderNo() +"&" + IOrderDetailView.VIEW_TYPE + "=" + ViewType.VIEW);
调用getViewCaption(View view)方法获取它的caption。
    private String getViewCaption(View view) {
        Class<? extends View> class1 = view.getClass();
        Menu annotation = class1.getAnnotation(Menu.class);
        String viewParameters = currentViewEvent.getParameters();
        //系统定义的菜单
        if (annotation != null) {
            if (viewParameters != null && !viewParameters.isEmpty()) {
                //以"&"为分隔符,分割ViewChangeEvent对象的parameters
                String[] split = viewParameters.split("&");
                //将第一个字符串再以"="为分隔符,分割字符串
                String[] keyValue = split[0].split("=");
                if (keyValue.length > 1) {
                    return I18NUtility.getValue(annotation.captionI18NKey(), annotation.caption()) + "-" + keyValue[1];
                } else {
                    return I18NUtility.getValue(annotation.captionI18NKey(), annotation.caption()) + "-" + keyValue[0];
                }
            }
            return I18NUtility.getValue(annotation.captionI18NKey(), annotation.caption());
        }
        //非系统定义的菜单
        else {
            if (viewParameters != null && !viewParameters.isEmpty()) {
                String[] split = viewParameters.split("&");
                String[] keyValue = split[0].split("=");
                if (keyValue.length > 1) {
                    if (keyValue[0].equals("i18n")) {
                        String[] split1 = keyValue[1].split(":");
                        return split1.length > 1 ? I18NUtility.getValue(split1[0], split1[1])
                            : I18NUtility.getValue(split1[0]);
                    }
                    return keyValue[1];
                } else {
                    return keyValue[0];
                }
            }
            return "";
        }
    }

最后解析成页面展示的caption,如下图

customized caption

13.5. 扩展系统应用或页面 — 高级篇

如果如上方法不能满足要求,比如说其他非Vaadin页面的应用嵌入,或者说Lumos应用下的扩展菜单的实现,那么此时你需要了解Lumos的应用加载策略。

在Lumos中,系统在启动时,会根据如下接口找寻系统中的所有应用信息:

package com.ags.lumosframework.web.vaadin.base;

import java.util.HashSet;
import java.util.Set;

import com.ags.lumosframework.web.common.functionality.ApplicationGroup;
import com.ags.lumosframework.web.common.functionality.ApplicationInfo;
import com.ags.lumosframework.web.common.functionality.ApplicationPageGroup;
import com.ags.lumosframework.web.common.functionality.ApplicationPageInfo;

/**
 * 用于扩展系统中菜单
 * <p>
 * 仍然可以利用@WebEntry向菜单中加入菜单分组信息等,该方式与其不冲突。
 * </tr>
 * 整个流程分为4个阶段,分别加载应用组,应用,菜单组,菜单等信息。方法的执行会按照顺序加载,即先加载所有实现类的ApplicationGroup,然后再加载所有实现类的Application。
 * </tr>
 * 所以该接口的扩展类可以仅仅选择实现这些接口里面的某些方法。
 * </tr>
 * 比如,如果需要向AdminUI里面加入定制化的某一个View,这个View不属于当前任何一个分组,那么实现者只需要实现loadApplicationPageGroup() 和loadApplicationPageInfo()即可
 *
 * @author yuri_li
 * @date 2019/08/01
 */
public interface IApplicationInfoLoader {

    /**
     * 向页面中心注册当前页面应用组的信息,该应用组表示应用所属应用的大类,如应用可以分为“运行期应用”,“编译期应用”,“管理相关应用”
     */
    void loadApplicationGroup();

    /**
     * 加载应用的信息,如工单,对象建模等模块,分属于ApplicationGroup
     */
    void loadApplication();

    /**
     * 加载应用信息里面的菜单分组信息,如建模里面的“工厂建模”,“人力资源”等菜单分页信息,分属于Application
     *
     */
    void loadApplicationPageGroup();

    /**
     * 加载具体的页面信息,分属于ApplicationPageGroup
     */
    void loadApplicationPageInfo();

    /**
     * 返回例外的ApplicationGroup,在菜单页里不会加载出来
     *
     * @return
     */
    default Set<ApplicationGroup> getExcludedApplicationGroups() {
        return new HashSet<>();
    };

    /**
     * 返回例外的ApplicationInfo,在菜单页里不会加载出来
     *
     * @return
     */
    default Set<ApplicationInfo> getExcludedApplicationInfos() {
        return new HashSet<>();
    };

    /**
     * 返回例外的ApplicationPageGroup,在菜单页里不会加载出来
     *
     * @return
     */
    default Set<ApplicationPageGroup> getExcludedApplicationPageGroups() {
        return new HashSet<>();
    };

    /**
     * 返回例外的ApplicationPageInfo,在菜单页里不会加载出来
     *
     * @return
     */
    default Set<ApplicationPageInfo> getExcludedApplicationPageInfos() {
        return new HashSet<>();
    };
}

正如如上接口声明的那样,整个应用的加载分为4个阶段,每个阶段独立运行,互不影响。 即,所有该接口实现类里面的loadApplicationGroup方法一定优先于任何一个实现类的loadApplication()执行。
所以,如果你需要扩展系统应用,可以选择实现该接口里面的一个方法即可。

getExcluded方法用于添加一些不想在页面显示的菜单页面,或菜单组,应用

可以参考如下实现,向Lumos系统中提供的ReportUI中加入更多的页面分组信息。

@Component
public class ReportApplicationLoader implements IApplicationInfoLoader {

    @Override
    public void loadApplicationGroup() {

    }

    @Override
    public void loadApplication() {

    }

    @Override
    public void loadApplicationPageGroup() {
        ApplicationInfo appInfoByAppPath = ApplicationManager.getAppInfoByAppPath(CommonConstants.REPORT);
        // 生产看板
        ApplicationPageGroup build = ApplicationPageGroup.builder().applicationInfo(appInfoByAppPath)
            .name(MesConstants.MENU_GROUP_Production_KanBan_Default)
            .captionI18NKey(MesConstants.MENU_GROUP_Production_KanBan_I18N)
            .caption(MesConstants.MENU_GROUP_Production_KanBan_Default)
            .iconPath("images/icon/report/ProductionDashboard.png").order(0).build();
        ApplicationManager.addApplicationPageGroup(build);

        // 投入产出
        build = ApplicationPageGroup.builder().applicationInfo(appInfoByAppPath)
            .name(MesConstants.MENU_GROUP_OUTPUT_Default).captionI18NKey(MesConstants.MENU_GROUP_OUTPUT_I18N)
            .caption(MesConstants.MENU_GROUP_OUTPUT_Default).iconPath("images/icon/report/Input-outputReport.png")
            .order(1).build();
        ApplicationManager.addApplicationPageGroup(build);

        // 在制
        build =
            ApplicationPageGroup.builder().applicationInfo(appInfoByAppPath).name(MesConstants.MENU_GROUP_WIP_Default)
                .captionI18NKey(MesConstants.MENU_GROUP_WIP_I18N).caption(MesConstants.MENU_GROUP_WIP_Default)
                .iconPath("images/icon/report/In-processReport.png").order(2).build();
        ApplicationManager.addApplicationPageGroup(build);

        // 工单
        build = ApplicationPageGroup.builder().applicationInfo(appInfoByAppPath)
            .name(MesConstants.MENU_GROUP_WorkOrder_Default).captionI18NKey(MesConstants.MENU_GROUP_WorkOrder_I18N)
            .caption(MesConstants.MENU_GROUP_WorkOrder_Default).iconPath("images/icon/report/WorkOrderReport.png")
            .order(3).build();
        ApplicationManager.addApplicationPageGroup(build);

        // 质量
        build = ApplicationPageGroup.builder().applicationInfo(appInfoByAppPath)
            .name(MesConstants.MENU_GROUP_Quality_Default).captionI18NKey(MesConstants.MENU_GROUP_Quality_I18N)
            .caption(MesConstants.MENU_GROUP_Quality_Default).iconPath("images/icon/report/QualityReport.png").order(4)
            .build();
        ApplicationManager.addApplicationPageGroup(build);
    }

    @Override
    public void loadApplicationPageInfo() {

    }

    /**
     * 移除administration相关模块,
     * @return
     */
    @Override
    public Set<ApplicationInfo> getExcludedApplicationInfos() {
        Set<ApplicationInfo> applicationPageInfos = new HashSet<>();
        ApplicationInfo administration = ApplicationInfo.builder().path("administration").build();
        applicationPageInfos.add(administration);
        return applicationPageInfos;
    }

    /**
     * 移除administration模块下的OC组
     * @return
     */
    @Override
    public Set<ApplicationPageGroup> getExcludedApplicationPageGroups() {
        Set<ApplicationPageGroup> applicationPageGroups = new HashSet<>();
        ApplicationInfo administration = ApplicationManager.getAppInfoByAppPath("administration");
        ApplicationPageGroup build = ApplicationPageGroup.builder().name(LumosConstants.ADMIN_OC_RELATED_Default)
            .applicationInfo(administration).build();
        applicationPageGroups.add(build);
        return applicationPageGroups;
    }

    /**
     * 移除administration模块下的custexttable(自定义表)菜单
     * @return
     */
    @Override
    public Set<ApplicationPageInfo> getExcludedApplicationPageInfos() {
        Set<ApplicationPageInfo> applicationPageInfos = new HashSet<>();
        ApplicationInfo administration = ApplicationManager.getAppInfoByAppPath("administration");
        ApplicationPageInfo custexttable =
            ApplicationPageInfo.builder().applicationInfo(administration).name("custexttable").build();
        applicationPageInfos.add(custexttable);
        return applicationPageInfos;
    }
}

当然,你也可以选择仅仅加入页面信息。

一定确保在具体的实现中加入对应目的的代码,比如在loadApplication中最好不要加载页面的信息,否则可能引起其他未知错误。

14. 在帮助页面新增资源文件

14.1. 实现原理

新建类实现Link控件信息接口,通过@Component注解让该类在View中可以被发现,然后获取其中的值生成Link控件

14.2. 实现流程

新建一个类,实现 IHelpItemInfo 接口并重写方法,并加上@Conponent注解

例如:

JavaApiDocLumos.class
package com.ags.lumosframework.web.vaadin.ui.help;
import com.ags.lumosframework.web.common.functionality.IHelpItemInfo;
import org.springframework.stereotype.Component;
@Component
public class JavaApiDocLumos implements IHelpItemInfo {

    //返回Link控件的标题
    @Override
    public String getCaption() {
        String caption = "JavaApiDoc-lumos"
        return caption;
    }

    //返回Link控件的链接地址
    @Override
    public String getPath() {
        String path = "help/apidocs-lumos/index.html"
        return path;
    }

    //返回控件位置优先级,越小优先级越高
    @Override
    public int getShowOrder() {
        int order = 1
        return order;
    }
}

15. 使用DB Tool产生DB帮助文档

15.1. pom文件新增插件

pom.xml
<plugin>
    <artifactId>lumos-chm-tool</artifactId>
    <version>3.0.0-SNAPSHOT</version>
    <groupId>com.ags.lumosframework</groupId>
    <dependencies>
        <dependency>
            <groupId>com.ags.mes</groupId>
            <artifactId>mes-core-api</artifactId>
            <version>4.0.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
    <executions>
        <execution>
            <id>generateHTML</id>
            <phase>package</phase>
            <goals>
                <goal>chm</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <packagePaths>
            <packagePath>
                com.ags.mes.server.api.entity
            </packagePath>
        </packagePaths>
    </configuration>
</plugin>

参数说明:

dependencies:将需要生成DB的Jar包填写进来

packagePaths:将Entity类的包路径填写进来

注:dependencies与packagePaths不存在对应关系

15.2. 添加注解

在类的属性上添加@Description注解,即可在生成的html中看到对应描述,例如:

DBTool1
DBTool2

15.3. 生成html文件

对使用插件的模块使用mvn package指令即可在模块的目录中看到DataDictionaryItem文件夹,里面就是生成的html文件

16. 系统提供的UI组件

16.1. 分页组件

目前,平台提供两种分页组件,一种是适用于任何对象的PaginationObjectListGrid分页组件,内部具体属性和方法如下

package com.ags.lumosframework.web.vaadin.component.paginationobjectlist;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import com.ags.lumosframework.sdk.base.pagination.PageInfo;
import com.ags.lumosframework.web.common.i18.I18NUtility;
import com.ags.lumosframework.web.vaadin.base.BaseView;
import com.github.appreciated.material.MaterialTheme;
import com.vaadin.data.ValueProvider;
import com.vaadin.data.provider.DataProvider;
import com.vaadin.data.provider.GridSortOrder;
import com.vaadin.data.provider.ListDataProvider;
import com.vaadin.data.provider.QuerySortOrder;
import com.vaadin.event.selection.SelectionEvent;
import com.vaadin.event.selection.SelectionListener;
import com.vaadin.icons.VaadinIcons;
import com.vaadin.shared.ui.MarginInfo;
import com.vaadin.ui.Alignment;
import com.vaadin.ui.Button;
import com.vaadin.ui.Button.ClickEvent;
import com.vaadin.ui.Button.ClickListener;
import com.vaadin.ui.Component;
import com.vaadin.ui.Grid;
import com.vaadin.ui.Grid.Column;
import com.vaadin.ui.Grid.SelectionMode;
import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.Label;
import com.vaadin.ui.VerticalLayout;
import com.vaadin.ui.components.grid.GridSelectionModel;
import com.vaadin.ui.components.grid.MultiSelectionModelImpl;
import com.vaadin.ui.components.grid.NoSelectionModel;
import com.vaadin.ui.components.grid.SingleSelectionModelImpl;
import com.vaadin.ui.renderers.AbstractRenderer;
import com.vaadin.ui.renderers.TextRenderer;
import com.vaadin.ui.themes.ValoTheme;

/**
 * 可以绑定任何对象,如果需要使用单表对象,建议参考{PaginationDomainObjedtList}
 *
 * @author yuri_li
 * @date 2019/08/08
 */
public class PaginationObjectListGrid<T> extends BaseView implements IObjectListGrid<T>, ClickListener {

    /**
     *
     */
    private static final long serialVersionUID = -3903777668282783935L;
    public static int LOADING_DATA_SIZE = 30;
    public Set<String> columnNameList = new HashSet<String>();
    protected Grid<T> grid = new Grid<>();
    List<GridSortOrder<T>> sortOrder = Collections.emptyList();
    private IObjectClickListener<T> objectClickListener = null;
    private IObjectSelectionListener<T> objectSelectionListener = null;
    private List<T> data = null;
    private boolean isLocalModel = false;
    @SuppressWarnings("unused")
    private boolean isDialog = false;

    private Label lblPageInfo = new Label();
    private Button btnCurrentPage = new Button();
    private Button btnFirstPage = new Button();
    private Button btnLastPage = new Button();
    private Button btnPreviousPage = new Button();
    private Button btnNextPage = new Button();
    private Button[] btns = {btnFirstPage, btnLastPage, btnCurrentPage, btnPreviousPage, btnNextPage};
    private int currentPage = 1;
    private int pageSize = LOADING_DATA_SIZE;
    private int totalCount = -1;
    private int startPosition = 0;

    protected IPagingQuery<T> pagingQuery = null;

    public PaginationObjectListGrid(IPagingQuery<T> queryFilter) {
        pagingQuery = queryFilter;
        initGrid();
        VerticalLayout vlRoot = new VerticalLayout();
        vlRoot.addStyleName(MaterialTheme.CARD_0 + " " + MaterialTheme.CARD_NO_PADDING);
        vlRoot.setSpacing(false);
        vlRoot.setSizeFull();
        vlRoot.addComponent(grid);
        vlRoot.setExpandRatio(grid, 1);
        vlRoot.setId("vl_root");
        setElementsId();

        HorizontalLayout hlTool = new HorizontalLayout();
        hlTool.setSpacing(false);
        hlTool.setMargin(new MarginInfo(true, false, false, false));
        hlTool.setDefaultComponentAlignment(Alignment.MIDDLE_RIGHT);
        hlTool.setWidth("100%");
        HorizontalLayout hlMsg = new HorizontalLayout();
        hlMsg.setWidth("100%");
        hlMsg.addComponent(lblPageInfo);
        hlTool.addComponents(hlMsg, btnFirstPage, btnPreviousPage, btnCurrentPage, btnNextPage, btnLastPage);
        hlTool.setExpandRatio(hlMsg, 1);

        btnCurrentPage.addStyleNames(MaterialTheme.BUTTON_FRIENDLY);
        btnCurrentPage.setEnabled(false);
        btnPreviousPage.setIcon(VaadinIcons.ANGLE_LEFT);
        btnNextPage.setIcon(VaadinIcons.ANGLE_RIGHT);
        btnFirstPage.setIcon(VaadinIcons.ANGLE_DOUBLE_LEFT);
        btnLastPage.setIcon(VaadinIcons.ANGLE_DOUBLE_RIGHT);
        for (Button btn : btns) {
            btn.setDisableOnClick(true);
            btn.addClickListener(this);
            btn.addStyleNames(MaterialTheme.BUTTON_ICON_ONLY, ValoTheme.BUTTON_SMALL);
        }

        vlRoot.addComponent(hlTool);
        vlRoot.setComponentAlignment(hlTool, Alignment.MIDDLE_RIGHT);
        this.setSizeFull();
        this.setCompositionRoot(vlRoot);
    }

    private void setElementsId() {
        lblPageInfo.setId("lbl_nowpage");
        btnCurrentPage.setId("btn_currentpage");
        btnFirstPage.setId("btn_firstpage");
        btnLastPage.setId("btn_lastpage");
        btnPreviousPage.setId("btn_previouspage");
        btnNextPage.setId("btn_nextpage");
    }

    private void initGrid() {
        grid.addStyleNames(MaterialTheme.GRID_BORDERLESS);
        grid.addColumn(new ValueProvider<T, String>() {
            private static final long serialVersionUID = 2776458444454640094L;

            @Override
            public String apply(T source) {
                return data.indexOf(source) + startPosition + 1 + "";
            }
        }).setCaption(I18NUtility.getValue("common.id", "ID")).setMinimumWidth(60);
        grid.addItemClickListener(event -> {
            if (objectClickListener != null) {
                objectClickListener.itemClicked(event);
            }
        });
        grid.addSelectionListener(new SelectionListener<T>() {
            private static final long serialVersionUID = 4404475737160537026L;

            @Override
            public void selectionChange(SelectionEvent<T> event) {
                if (objectSelectionListener != null) {
                    objectSelectionListener.itemClicked(event);
                }
            }
        });
        grid.addSortListener(listener -> {
            sortOrder = listener.getSortOrder();
            refresh();
        });
        grid.setSizeFull();
    }

    @Override
    public void setStartPage(int currentPage, int startPosition) {
        this.currentPage = currentPage;
        this.startPosition = startPosition;
    }

    @Override
    public void setPageSize(int pageSize) {
        this.pageSize = pageSize;
    }

    @Override
    public Column<T, ?> addColumn(String propertyName) {
        return grid.addColumn(propertyName, new TextRenderer()).setSortOrderProvider(direction -> {
            List<QuerySortOrder> list = new ArrayList<>();
            list.add(new QuerySortOrder(propertyName, direction));
            return list.stream();
        }).setHidable(true);
    }

    @Override
    public Column<T, ?> addColumn(String propertyName, AbstractRenderer<? super T, ?> renderer) {
        return grid.addColumn(propertyName, renderer).setSortOrderProvider(direction -> {
            List<QuerySortOrder> list = new ArrayList<>();
            list.add(new QuerySortOrder(propertyName, direction));
            return list.stream();
        }).setHidable(true);
    }

    @Override
    public <V> Column<T, V> addColumn(ValueProvider<T, V> valueProvider) {
        return grid.addColumn(valueProvider, new TextRenderer()).setHidable(true);
    }

    @Override
    public <V> Column<T, V> addColumn(ValueProvider<T, V> valueProvider,
        AbstractRenderer<? super T, ? super V> renderer) {
        return grid.addColumn(valueProvider, renderer).setHidable(true);
    }

    @Override
    public <V> Column<T, V> addColumn(ValueProvider<T, V> valueProvider,
        ValueProvider<V, String> presentationProvider) {
        return grid.addColumn(valueProvider, presentationProvider).setHidable(true);
    }

    @Override
    public <V, P> Column<T, V> addColumn(ValueProvider<T, V> valueProvider, ValueProvider<V, P> presentationProvider,
        AbstractRenderer<? super T, ? super P> renderer) {
        return grid.addColumn(valueProvider, presentationProvider, renderer).setHidable(true);
    }

    @Override
    public <V extends Component> Column<T, V> addComponentColumn(ValueProvider<T, V> componentProvider) {
        return grid.addComponentColumn(componentProvider).setHidable(true);
    }

    @Override
    public void setData(List<T> data) {
        this.data = data;
        this.isLocalModel = true;
    }

    /*
     * (non-Javadoc)
     *
     * @see com.ags.jaspermes.ui.component.IDomainObjectGrid#refresh()
     */
    @Override
    public void refresh() {
        if (isLocalModel) {
            ListDataProvider<T> ofCollection = DataProvider.ofCollection(data);
            grid.setDataProvider(ofCollection);
            if (!data.isEmpty()) {
                grid.select(data.get(0));
            }
        } else {
            pagingQuery.init();
            totalCount = pagingQuery.count();

            data = pagingQuery.list(new PageInfo(currentPage, pageSize)).getRecords();
            ListDataProvider<T> ofCollection = DataProvider.ofCollection(data);
            grid.setDataProvider(ofCollection);
            updateButtonStatus();
            updatePageInfo();
        }
    }

    @Override
    public void refresh(T item) {
        grid.getDataProvider().refreshItem(item);
    }

    @Override
    public void setObjectClickListener(IObjectClickListener<T> listener) {
        this.objectClickListener = listener;
    }

    @Override
    public void setObjectSelectionListener(IObjectSelectionListener<T> listener) {
        this.objectSelectionListener = listener;
    }

    @Override
    public T getSelectedObject() {
        Iterator<T> iterator = grid.getSelectedItems().iterator();
        if (iterator.hasNext()) {
            return iterator.next();
        } else {
            return null;
        }
    }

    @Override
    public void addStyleNameToGrid(String style) {
        grid.addStyleName(style);
    }

    @Override
    public SelectionMode getSelectionMode() {
        GridSelectionModel<T> selectionModel = grid.getSelectionModel();
        SelectionMode mode = null;
        if (selectionModel.getClass().equals(SingleSelectionModelImpl.class)) {
            mode = SelectionMode.SINGLE;
        } else if (selectionModel.getClass().equals(MultiSelectionModelImpl.class)) {
            mode = SelectionMode.MULTI;
        } else if (selectionModel.getClass().equals(NoSelectionModel.class)) {
            mode = SelectionMode.NONE;
        }
        return mode;
    }

    @Override
    public void setSelectionMode(SelectionMode selectionMode) {
        grid.setSelectionMode(selectionMode);
    }

    @Override
    public List<T> getSelections() {
        List<T> selections = new ArrayList<T>();
        selections.addAll(grid.getSelectedItems());
        return selections;
    }

    @Override
    public void select(List<T> selected) {
        grid.deselectAll();
        selected.forEach(grid::select);
    }

    @Override
    public void setSort(String columnName) {
        columnNameList.add(columnName);
    }

    @Override
    public void clearSortColumnName() {
        columnNameList.clear();
    }

    @Override
    public void setIsDialog(boolean isDialog) {
        this.isDialog = isDialog;
    }

    @Override
    public void buttonClick(ClickEvent event) {
        Button button = event.getButton();
        button.setEnabled(true);
        if (button.equals(btnFirstPage)) {
            currentPage = 1;
            startPosition = 0;
        } else if (button.equals(btnLastPage)) {
            currentPage = totalCount % pageSize == 0 ? totalCount / pageSize : totalCount / pageSize + 1;;
            startPosition = (currentPage - 1) * pageSize;
        } else if (button.equals(btnPreviousPage)) {
            if (currentPage - 1 != 0) {
                currentPage--;
            }
            startPosition = (currentPage - 1) * pageSize;
        } else if (button.equals(btnNextPage)) {
            if (currentPage * pageSize < totalCount) {
                currentPage++;
            }
            startPosition = (currentPage - 1) * pageSize;

        }
        updateButtonStatus();
        updatePageInfo();
        refresh();
        grid.scrollToStart();
    }

    private void updateButtonStatus() {
        if (startPosition + pageSize < totalCount) {
            btnNextPage.setEnabled(true);
            btnLastPage.setEnabled(true);
        } else {
            btnNextPage.setEnabled(false);
            btnLastPage.setEnabled(false);
        }

        if (currentPage == 1) {
            btnFirstPage.setEnabled(false);
            btnPreviousPage.setEnabled(false);
        } else {
            btnFirstPage.setEnabled(true);
            btnPreviousPage.setEnabled(true);
        }
    }

    private void updatePageInfo() {
        int i = totalCount % pageSize;
        int page = totalCount / pageSize;
        int pageCount = page + (i == 0 && page != 0 ? 0 : 1);
        btnCurrentPage.setCaption(String.valueOf(currentPage));
        if (columnNameList.size() > 2) {
            lblPageInfo.setValue(I18NUtility.getValue("common.page.totalcount", "Total Count") + ": "+ totalCount + " | "
                + I18NUtility.getValue("common.page.totalpage", "Total Page") + ": " + pageCount);
        } else {
            lblPageInfo.setValue(I18NUtility.getValue("common.page.totalpage", "Total Page") + ": " + pageCount);
        }
    }

    @Override
    public void setFilter(Object filter) {
        pagingQuery.setFilter(filter);
    }
}

另外一种是针对于单表分页的PaginationDomainObjectList分页组件,它继承了PaginationObjectListGrid,根据实际需求是否调用 attachFixedColumns(boolean hasName),然后会根据代码执行顺序先后加入固定的栏位: 名称、创建人,创建时间,最后修改人,最后修改时间。

根据实际情况放置attachFixedColumns(boolean hasName)方法,一般放在所有特有属性的column添加的代码之后。
例子:工单数据分页显示
private IDomainObjectGrid<WorkOrder> gridOrder = new PaginationDomainObjectList<>();

页面具体效果如图pagination.png所示

core\pagination

16.2. 系统配置项组件

系统目前定义的所有的配置项都是在页面初始化的时候调用loadConfigurations()方法,ConfigurationService中的
getByNameAndCategory(String name, String category)方法是根据配置项的名称和类别进行查询,如果该配置项不存
在,那么将会按照所传入的名称和类别创建一个配置项,并且保存。

如果想要添加系统配置项,先定义两个常量,即Configuration的Category和Name。

系统已经定义的配置项组件和如何创建对应的对象:

ComboBoxConfigurationItem: 复选框

private ComboBoxConfigurationItem<MediaStorageProtocol> comboBox = new ComboBoxConfigurationItem<>(false);

PasswordConfigurationItem: 密码框

private PasswordConfigurationItem password = new PasswordConfigurationItem(false);

BooleanConfigurationItem: 单选框

private BooleanConfigurationItem booleanSelect = new BooleanConfigurationItem(false);

TextConfigurationItem: 文本输入框

private TextConfigurationItem text = new TextConfigurationItem(false)

ConfigurationItemPanel: 每个配置项页面显示的Panel

ConfigurationItemPanel panel = new ConfigurationItemPanel();

ConfigurationItemDialog: 配置项的编辑弹窗

private IConfigurationItemDialog dialog;

dialog = BeanManager.getService(IConfigurationItemDialog.class);

ConfigurationItemPanel具体使用
    /**
    * 页面添加小数点精确位数的设置
    */
    private void addDigitConfigurationPanel() {
        //创建ConfigurationItemPanel对象
        ConfigurationItemPanel digitCfgPanel = new ConfigurationItemPanel();
        digitCfgPanel.setId("pl_digit");
        //定义caption
        digitCfgPanel.setCaption(I18NUtility.getValue("Admin.System.Setting.DigitSetting", "Exact Digit Configuration"));
        //ConfigurationItemPanel中添加private TextConfigurationItem digit = new TextConfigurationItem(false)组件;
        digitCfgPanel.addConfigurationItem(digit);
        //系统配置页面添加ConfigurationItemPanel组件
        vlContent.addComponent(digitCfgPanel);

        digitCfgPanel.addItemSaveCallback(new ConfigurationItemSaveCallBack() {

            @Override
            public void done(ConfirmResult result) {
                if (ConfirmResult.Result.OK.equals(result.getResult())) {
                    @SuppressWarnings("unchecked")
                    Map<String, BaseConfigurationItem<?>> items =(Map<String, BaseConfigurationItem<?>>)result.getObj();
                    BaseConfigurationItem<?> configurationItem = items.get(digit.getCaption());
                    String digit = configurationItem.getValue().toString();
                    Configuration config = presenter.getDigitConfig();
                    config.setValue(digit);
                    presenter.saveDigitConfig();
                    loadConfigurations();
                }
            }
        });
    }

16.3. 表达式校验组件

目前系统中只有在添加物料中有使用到该组件

rule validate

使用该组件,先定义private MatcherExpressionEditor meValidateRules = new MatcherExpressionEditor()组件对象, 就像使用普通组件一样。MatcherExpressionEditor中定义了一个文本输入框和一个编辑按钮,点击编辑按钮,会出现图片 右侧的编辑表达式的弹窗 EditMatcherExpressionDialog

表达式的编辑提供五中类型选择
 //固定值
 CONSTANT("Constant", "Configuration.ExpressionType.Constant", ""),
 //数字
 NUMBER("Number", "Configuration.ExpressionType.Number", "[0-9]"),
 //字母
 CHARACTER("Character", "Configuration.ExpressionType.Character", "[a-zA-Z]"),
 //数字或字母
 NUMBER_AND_CHARACTER("NumberAndCharacter", "Configuration.ExpressionType.NumberAndCharacter", "[a-zA-Z0-9]"),
 //特殊字符
 SPECIAL_CHARACTER("SpecialCharacter", "Configuration.ExpressionType.SpecialCharacter", "[!@#$%^&*()_+=\\-\\s]");

校验规则的使用

例如我们在新建物料设置了序列号校验,在操作中心物料过站时,扫描序列号时需要验证序列号是否符合校验规则,调用 MatcherExpressionItem中的toMatcherExpression(String validateRules)方法,返回一个正则表达式,然后调用Pattern 的matches方法判断是否匹配。

MatcherExpressionItem 代码
package com.ags.lumosframework.web.vaadin.component.checkexpression;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import com.ags.lumosframework.common.constant.CommonConstants;
import com.ags.lumosframework.common.enums.MatcherExpressionType;
import com.ags.lumosframework.web.common.i18.I18NUtility;
import com.google.common.base.Strings;

public class MatcherExpressionItem {

    private MatcherExpressionType itemType;
    private String itemValue;
    private int no;
    private MatcherExpressionTypeChangeListener typeChangeListener;

    public MatcherExpressionItem() {}

    public MatcherExpressionItem(int no) {
        this.no = no + 1;
    }

    public MatcherExpressionItem(int no, MatcherExpressionType type, String itemValue) {
        this.no = no;
        this.itemType = type;
        this.itemValue = itemValue;
    }

    public static List<MatcherExpressionItem> parseExpression(String persistenceExpression) {
        List<MatcherExpressionItem> items = new ArrayList<>();
        if (!Strings.isNullOrEmpty(persistenceExpression)) {
            String[] arrItems = persistenceExpression.split("\\" + CommonConstants.EXPRESSION_CONNECTOR);
            MatcherExpressionItem itemTemp = null;
            for (String arrItem : arrItems) {
                String valueOrLen = "";
                String typeTemp = arrItem;
                if (arrItem.indexOf('(') >= 0) {
                    arrItem = arrItem.replace(")", "");
                    String[] split = arrItem.split("\\(");
                    typeTemp = split[0];
                    valueOrLen = split[1];
                }
                MatcherExpressionType type = MatcherExpressionType.getValue(typeTemp);
                itemTemp = new MatcherExpressionItem(items.size());
                itemTemp.setItemType(type);
                itemTemp.setItemValue(valueOrLen);
                items.add(itemTemp);
            }
        }
        return items;
    }

    public static String getDisplay(List<MatcherExpressionItem> items) {
        StringBuilder res = new StringBuilder();
        items.sort((item1, item2) -> item1.getNo() - item2.getNo());
        for (MatcherExpressionItem item : items) {
            res.append(I18NUtility.getValue(item.getItemType().getNameKey(), item.getItemType().getName()));
            res.append("(" + item.getItemValue() + ")");
            res.append(CommonConstants.EXPRESSION_CONNECTOR);
        }
        String strRes = res.toString();
        if (strRes.endsWith(CommonConstants.EXPRESSION_CONNECTOR)) {
            strRes = strRes.substring(0, strRes.lastIndexOf(CommonConstants.EXPRESSION_CONNECTOR));
        }
        return strRes;
    }

    public static String toMatcherExpression(String persistenceExpression) {
        return toMatcherExpression(parseExpression(persistenceExpression));
    }

    public static String toMatcherExpression(List<MatcherExpressionItem> items) {
        StringBuilder res = new StringBuilder();
        for (int index = 0; index < items.size(); index++) {
            MatcherExpressionItem item = items.get(index);
            if (index == 0) {
                res.append('^');
            }
            if (MatcherExpressionType.CONSTANT.equals(item.getItemType())) {
                res.append(item.getItemValue());
            } else {
                res.append(item.getItemType().getMatcherExpression());
                res.append('{');
                res.append(item.getItemValue());
                res.append('}');
            }
            if (index == (items.size() - 1)) {
                res.append('$');
            }
        }
        return res.toString();
    }

    public static String toPersistenceExpression(List<MatcherExpressionItem> items) {
        StringBuilder res = new StringBuilder();
        items.sort((item1, item2) -> item1.getNo() - item2.getNo());
        for (MatcherExpressionItem item : items) {
            res.append(item.getItemType().getName());
            res.append("(" + item.getItemValue() + ")");
            res.append(CommonConstants.EXPRESSION_CONNECTOR);
        }
        String strRes = res.toString();
        if (strRes.endsWith(CommonConstants.EXPRESSION_CONNECTOR)) {
            strRes = strRes.substring(0, strRes.lastIndexOf(CommonConstants.EXPRESSION_CONNECTOR));
        }
        return strRes;
    }

    public MatcherExpressionType getItemType() {
        return itemType;
    }

    public void setItemType(MatcherExpressionType itemType) {
        this.itemType = itemType;
        if (Objects.nonNull(typeChangeListener)) {
            typeChangeListener.typeChange(itemType);
        }
    }

    public String getItemValue() {
        return itemValue;
    }

    public void setItemValue(String itemValue) {
        this.itemValue = itemValue;
    }

    public int getNo() {
        return no;
    }

    public void setNo(int no) {
        this.no = no;
    }

    public MatcherExpressionTypeChangeListener getTypeChangeListener() {
        return typeChangeListener;
    }

    public void setTypeChangeListener(MatcherExpressionTypeChangeListener typeChangeListener) {
        this.typeChangeListener = typeChangeListener;
    }

    public interface MatcherExpressionTypeChangeListener {
        void typeChange(MatcherExpressionType type);
    }

}

16.4. 对象选择框

如何定义一个对象选择框

例如系统中异常代码选择处理代码组

failurecode
FailureCodeViewImpl页面具体实现
     @Inject
     private IObjectSelectDialog objectSelectDialog;

     else if (btnModifyActionGroup.equals(button)) {
         //设置后台服务的名称
         objectSelectDialog.setService(IFailureActionGroupService.class);
         objectSelectDialog.setSort(FailureActionCodeGroupEntity.NAME);
         objectSelectDialog.setIsDialog(true);
         //是否必选
         objectSelectDialog.setMustSelect(false);
         List<BuildtimeObjectBaseImpl<?>> fcs = new ArrayList<>();
         if (failureCode.getFailureActionCodeGroup() != null) {
             fcs.add(failureCode.getFailureActionCodeGroup());
         }
         //设置选择框的caption
         objectSelectDialog.setCaption(I18NUtility.getValue("Common.Select", "Select") + " "
             + I18NUtility.getValue("Configuration.Failure.FailureActionCodeGroup", "Failure Action Code Group"));
          //设置已经被绑定的数据页面展示是选中状态
         objectSelectDialog.select(fcs);
         objectSelectDialog.show(getUI(), new DialogCallBack() {
             @Override
             public void done(ConfirmResult result) {
                 if (result.getResult().equals(Result.OK)) {
                     FailureActionCodeGroup actionGroup = (FailureActionCodeGroup)result.getObj();
                     failureCode.setFailureActionCodeGroup(actionGroup);
                     presenter.saveFailureCode(failureCode);
                     lblFailureActionGroup.setValue(actionGroup == null ? "" : actionGroup.getActionCodeGroupName());
                 }
             }
         });
     }

对象选择框中还有一些其他方法,如setSelectionMode(SelectionMode selectionMode)是设置选择框是单选还是双选,默认是单选。如果加入自定义的方法,可以继承 IObjectSelectDialog接口,再添加其他方法。

IObjectSelectDialog接口定义
package com.ags.lumosframework.web.vaadin.component.objectselect;

import java.util.List;

import com.ags.lumosframework.sdk.base.domain.ObjectBaseImpl;
import com.ags.lumosframework.sdk.base.filter.EntityFilter;
import com.ags.lumosframework.web.vaadin.base.IBaseDialog;
import com.vaadin.ui.Grid.SelectionMode;

public interface IObjectSelectDialog<T extends ObjectBaseImpl<?>> extends IBaseDialog {
    void setService(Class<?> serviceClass);

    void setCaption(String caption);

    void setSelectionMode(SelectionMode selectionMode);

    void select(List<T> selected);

    void selectOne(T selected);

    List<T> getSelections();

    void setData(List<T> datas);

    void setIsDialog(boolean isDialog);

    void setMustSelect(boolean mustSelect);

    void setSort(String columnName);

    void setEntityFilter(EntityFilter entityFilter);
}

16.5. 二维码组件

系统页首面的二维码显示也是通过配置项进行设置的,在配置项中设置好对应值,在首页面就可以看到一个二维码图片

two dimensional code
生成二维码的restful接口
    /**
     * 获取主项目和移动端配置的参数 生成二维码图片
     *
     * @return
     */
    @ApiOperation(value = "获取主项目和移动端配置", notes = "get config mobile")
    @RequestMapping(value = "/getConfigMoible", method = RequestMethod.GET)
    public RestResponse<String> getConfigMoible() {
        //查询已经配置好的主项目和移动端配置的参数
        Configuration mainProjectGatewayIp = configurationService.getByNameAndCategory(
            ConfigurationConstant.CFG_MAIN_PROJECT_GATEWAY_IP, ConfigurationConstant.CATEGORY_SYSTEM_MAIN_PROJECT);
        Configuration mainProjectGatewayPort = configurationService.getByNameAndCategory(
            ConfigurationConstant.CFG_MAIN_PROJECT_GATEWAY_PORT, ConfigurationConstant.CATEGORY_SYSTEM_MAIN_PROJECT);
        Configuration mainProjectName = configurationService.getByNameAndCategory(
            ConfigurationConstant.CFG_MAIN_PROJECT_NAME, ConfigurationConstant.CATEGORY_SYSTEM_MAIN_PROJECT);
        Configuration mobileProjectGatewayIp = configurationService.getByNameAndCategory(
            ConfigurationConstant.CFG_MOBILE_PROJECT_GATEWAY_IP, ConfigurationConstant.CATEGORY_SYSTEM_MOBILE);
        Configuration mobileProjectGatewayPort = configurationService.getByNameAndCategory(
            ConfigurationConstant.CFG_MOBILE_PROJECT_GATEWAY_PORT, ConfigurationConstant.CATEGORY_SYSTEM_MOBILE);
        Configuration mobileProjectName = configurationService.getByNameAndCategory(
            ConfigurationConstant.CFG_MOBILE_PROJECT_NAME, ConfigurationConstant.CATEGORY_SYSTEM_MOBILE);
        if(StringUtils.isEmpty(mainProjectGatewayIp.getValue())) {
            return new RestResponse<>(RestResponseCode.NOTFOUND, true, "success", "");
        }
        Map<String, String> valueMap = new HashMap<>();
        valueMap.put(ConfigurationConstant.SHOW_MAIN_PROJECT_GATEWAY_IP,
            mainProjectGatewayIp == null ? "" : mainProjectGatewayIp.getValue());
        valueMap.put(ConfigurationConstant.SHOW_MAIN_PROJECT_GATEWAY_PORT,
            mainProjectGatewayPort == null ? "" : mainProjectGatewayPort.getValue());
        valueMap.put(ConfigurationConstant.SHOW_MAIN_PROJECT_NAME,
            mainProjectName == null ? "" : mainProjectName.getValue());
        valueMap.put(ConfigurationConstant.SHOW_MOBILE_PROJECT_GATEWAY_IP,
            mobileProjectGatewayIp == null ? "" : mobileProjectGatewayIp.getValue());
        valueMap.put(ConfigurationConstant.SHOW_MOBILE_PROJECT_GATEWAY_PORT,
            mobileProjectGatewayPort == null ? "" : mobileProjectGatewayPort.getValue());
        valueMap.put(ConfigurationConstant.SHOW_MOBILE_PROJECT_NAME,
            mobileProjectName == null ? "" : mobileProjectName.getValue());
        // 转map换成json 调用生成二维码图片
        Object valuMapJson = JSONObject.toJSON(valueMap);
        byte[] byteArray = QRCodeUtils.generateQRCode(valuMapJson.toString());
        String byteString = Base64Utils.encodeToString(byteArray);
        return new RestResponse<>(RestResponseCode.OK, true, "success", byteString);
    }
生成二维码的工具类
package com.ags.lumosframework.common.util;

import java.io.ByteArrayOutputStream;
import java.util.Hashtable;
import java.util.Map;

import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.client.j2se.MatrixToImageConfig;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;

import lombok.extern.slf4j.Slf4j;

/**
 * 转成二维码
 *
 * @author andrew_he
 *
 */
@Slf4j
public class QRCodeUtils {

    private static int onColor = 0xFF000000;     //前景色
    private static int offColor = 0xFFFFFFFF;    //背景色

    public static byte[] generateQRCode(String value) {
        byte[] byteArray = null;
        Map<EncodeHintType, Object> hints = new Hashtable<EncodeHintType, Object>();
        // 指定纠错等级
        hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);
        // 指定编码格式
        hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
        try {
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            BitMatrix bitMatrix = new MultiFormatWriter().encode(value, BarcodeFormat.QR_CODE, 500, 500, hints);
            MatrixToImageConfig config = new MatrixToImageConfig(onColor, offColor);
            MatrixToImageWriter.writeToStream(bitMatrix, "png", byteArrayOutputStream, config);
            byteArray = byteArrayOutputStream.toByteArray();
        } catch (Exception e) {
            log.error(e.getMessage());
        }
        return byteArray;
    }

    public static void generateQRCodeImage(String value) {

    }

}
除此之外,在QRCodeImage组件中也提供了二维码的生成和页面显示
Unresolved directive in core/uicomponents.adoc - include::/var/jenkins_home/workspace/lumos-doc/lumos-framework/lumos-common/src/main/java/com/ags/lumosframework/web/vaadin/component/barcode4j/QRCodeImage.java[]

16.6. 扩展属性编辑组件

在上文中提到了系统对象扩展属性添加,例如创建Part的扩展属性,则平台会在数据库中创建CF_PART表用于保存Part的扩展属性值。我们会在Part页面 放一个编辑扩展属性的button,点击button,扩展属性编辑弹窗显示已经添加的扩展属性,我们填写的值会被保存在CF_PART表中。

part customized

Part页面具体调用
    // 扩展属性按钮
    private Button btnCF = new Button();

    //扩展属性编辑组件
    @Inject
    private IEditCustomizedFieldDialog cfDialog;

    if (clickedButton.equals(btnCF)) {
        Part selectAccount = (Part)this.objectGrid.getSelectedObject();
        cfDialog.setObjectExtension(selectAccount);
        cfDialog.show(getUI(), null);
    }

16.7. 公共属性展示组件

页面创建的对象都继承了BuildtimeBaseEntity,所以都具有部分公共属性,例如:名称、类别、创建人、创建时间、最后修改人、最后修 改时间以及描述。之前系统数据没有采用分页,一般只在左侧展示名称和描述,所以添加了一个公共属性展示的View组件,目前只有少数 页面在使用。

页面显示效果

common properties
引用实例
    @Inject
    private ICommonPropertiesView commonProperties;

    //点击选中数据时,右侧显示公共属性
    protected void setDataToRight(BuildtimeObjectBaseImpl<?> object) {
        commonProperties.setFullSize();
        if (object != null) {
            if (object instanceof QtagRootCauseCategory) {
                commonProperties
                    .setCaptionName(I18NUtility.getValue("Configuration.RootCauseCategory", "Root Cause Category"));
            } else {
                commonProperties
                    .setCaptionName(I18NUtility.getValue("Configuration.RootCauseDetail", "Root Cause Detail"));
            }
        } else {
            commonProperties.setCaptionName("");
        }
        commonProperties.setObject(object);
    }

16.8. RoundImage组件

页面具体应用和展示效果如下

core\round image

该组件就是在HorizontalLayout中定义了一个Image和Button,具体调用时添加按钮的点击监听事件

    private RoundImage image = null;

    image = new RoundImage();
    image.addDeleteClickListener(new ClickListener() {
        /**
         *
         */
        private static final long serialVersionUID = 1192024051414142281L;

        @Override
        public void buttonClick(ClickEvent event) {
            //点击时页面移除该组件
            hl.removeComponent(image);
            uploadEvent = null;
            image = null;
        }
    });

16.9. 文本框组件

该组件非常简单,只是对TextArea组件进行简易的封装,调用时就和普通弹窗一样使用,页面效果图如下

note dialog

16.10. ChartJS 组件

Lumos本身提供了chart js的支持,包括饼图、柱状图、折线图等等。

详细请参阅官方demo:
Vaadin ChartJS 官方文档

16.10.1. UI中添加chart

如果想在页面上添加一个BarChart,代码示例如下:

BarChartConfig barConfig = new BarChartConfig();
barConfig.data().labels("January", "February", "March", "April", "May", "June", "July")
    .addDataset(new BarDataset().backgroundColor(ChartThemeUtils.getBarChartBackgroundColor(0))
        .label("Dataset 1").yAxisID("y-axis-1"))
    .addDataset(new BarDataset().backgroundColor(ChartThemeUtils.getBarChartBackgroundColor(1))
        .label("Dataset 2").yAxisID("y-axis-2").hidden(true))
    .addDataset(new BarDataset().backgroundColor(ChartThemeUtils.getBarChartBackgroundColors(charData.size()))
        .label("Dataset 3").yAxisID("y-axis-1"))
    .and();

ChartJs barChart = new ChartJs(barConfig);
...

HorizontalLayout hlBarChart = new HorizontalLayout();
hlBarChart.addComponent(barChart);

16.10.2. chart中颜色值的设置

为了更好的匹配我们的浅色和深色主题,平台在 ChartThemeUtils 工具类中提供了常用的颜色集,供用户使用。
可以在 com.ags.lumosframework.web.vaadin.utility 包下找到此工具类。

16.10.3. Maven 依赖信息

<dependency>
    <groupId>com.byteowls</groupId>
    <artifactId>vaadin-chartjs</artifactId>
    <version>1.4.0</version>
    <scope>compile</scope>
</dependency>

16.11. 查询条件组件(SearchPanelBuilder)

lumos提供了自定义的查询组件,用于支持对象的多条件查询,风格统一而且便于维护。

16.11.1. 添加Conditions

Conditions即查询条件,在添加查询条件参数时,条件信息来源于对 IConditions 接口的实现,接口定义参阅如下代码:

package com.ags.lumosframework.web.vaadin.component.searchpanel.conditions;

import com.vaadin.ui.AbstractLayout;
import com.vaadin.ui.Component;

public interface IConditions {

    Object getFilter();

    void reset();

    /**
     * 返回查询条件的控件信息。 这个方法返回直接显示在搜索区域的所有控件(不建议 放多于1个 会影响布局),将直接显示在搜索区域,对于其他的条件,调用下面的方法返回其余的控件 以及布局。
     *
     * @return
     */
    Component[] getComponent();

    /**
     * 返回点击更多下的布局信息
     *
     * @return
     */
    AbstractLayout getLayout();
}

接口实现以operation对象为例,代码如下:

其中一些注意事项参考代码中的注释部分。

public OperationConditions() {
    private TextField tfOperationName = new TextField();

    @I18Support(caption = "Category", captionKey = "Common.Category")
    private ComboBox<OperationType> cbCategory = new ComboBox<>();

    private HasValue<?>[] fields = {tfOperationName, cbCategory};

    //components中的组件将被添加至search bar中。
    private Component[] components = {tfOperationName};

    //temp中的组件将被添加至Condition中,显示在弹出框中。
    private Component[] temp = {cbCategory};

    //显示在search bar中的控件不能有caption,请用Placeholder来代替。
    tfOperationName.setPlaceholder(I18NUtility.getValue("Common.Name", "Name"));

    //根容器请不要设置宽度100%,否则弹出框的宽度会与屏幕一样宽,即铺满整个屏幕。
    hlRoot.setWidthUndefined();

    for (Component component : temp) {
        //同样,弹出框中的组件宽度也需要设置为Undefined。
        component.setWidthUndefined();

        hlRoot.addComponent(component);
    }
}

16.11.2. SearchPanelBuilder的实现

代码如下:

package com.ags.lumosframework.web.vaadin.component.searchpanel;

import org.vaadin.addons.popupextension.PopupExtension;
import org.vaadin.artur.KeyAction;
import org.vaadin.artur.KeyAction.KeyActionEvent;
import org.vaadin.artur.KeyAction.KeyActionListener;

import com.ags.lumosframework.web.common.i18.I18NUtility;
import com.ags.lumosframework.web.vaadin.base.BaseComponent;
import com.ags.lumosframework.web.vaadin.base.CoreTheme;
import com.ags.lumosframework.web.vaadin.base.IFilterableView;
import com.ags.lumosframework.web.vaadin.component.paginationobjectlist.IObjectListGrid;
import com.ags.lumosframework.web.vaadin.component.searchpanel.conditions.IConditions;
import com.github.appreciated.material.MaterialTheme;
import com.vaadin.event.ShortcutAction.KeyCode;
import com.vaadin.icons.VaadinIcons;
import com.vaadin.spring.annotation.SpringComponent;
import com.vaadin.spring.annotation.UIScope;
import com.vaadin.ui.AbstractComponent;
import com.vaadin.ui.AbstractLayout;
import com.vaadin.ui.Alignment;
import com.vaadin.ui.Button;
import com.vaadin.ui.Button.ClickEvent;
import com.vaadin.ui.Button.ClickListener;
import com.vaadin.ui.Component;
import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.VerticalLayout;
import com.vaadin.ui.themes.ValoTheme;

import java.util.Iterator;

@UIScope
@SpringComponent
public class SearchPanelBuilder extends BaseComponent implements ISearchPanelBuilder, ClickListener {

    /**
     *
     */
    private static final long serialVersionUID = -8843066905247275781L;

    private IObjectListGrid<?> objectListGrid;

    private Button btnSearch = new Button();
    private Button btnReset = new Button();
    private Button btnMore = new Button();
    private PopupExtension popupExtension = null;
    private HorizontalLayout hlSearchBar = new HorizontalLayout();

    private IConditions condition;

    private IFilterableView view;

    public SearchPanelBuilder(IConditions condition, IObjectListGrid<?> objectListGrid, IFilterableView view) {
        hlSearchBar = new HorizontalLayout();
        this.objectListGrid = objectListGrid;
        this.view = view;
        this.condition = condition;
        initSearchBar();
        initPopupPanel();
    }

    public SearchPanelBuilder(IConditions condition, IObjectListGrid<?> objectListGrid) {
        this(condition, objectListGrid, null);
    }

    private void initSearchBar() {
        if (condition == null) {
            return;
        }
        hlSearchBar.setSizeFull();
        hlSearchBar.setSpacing(false);
        hlSearchBar.setMargin(false);
        hlSearchBar.setDefaultComponentAlignment(Alignment.MIDDLE_RIGHT);
        btnSearch.setId("btn_search");
        btnReset.setId("btn_reset");
        btnMore.setId("btn_more");
        // 搜索组合框
        HorizontalLayout hlSearch = new HorizontalLayout();
        hlSearch.addStyleName(MaterialTheme.LAYOUT_COMPONENT_GROUP_MATERIAL);
        hlSearch.setSpacing(false);

        // 组合框中的输入框
        Component[] components = condition.getComponent();
        for (int i = 0; i < components.length; i++) {
            Component c = components[i];
            c.setId("input_search" + (i == 0 ? "" : i));
            // c.focus();
            hlSearch.addComponent(c);
        }
        // 组合框中的查询按钮
        hlSearch.addComponent(btnSearch);
        btnSearch.setIcon(VaadinIcons.SEARCH);
        btnSearch.addStyleName(ValoTheme.BUTTON_PRIMARY);
        btnSearch.addClickListener(this);
        // btnSearch.addShortcutListener(new ShortcutListener(null, ShortcutAction.KeyCode.ENTER, null) {
        // private static final long serialVersionUID = 1L;
        //
        // @Override
        // public void handleAction(Object sender, Object target) {
        // try {
        // search();
        // } catch (Exception e) {
        // log.error("", e);
        // NotificationUtils.notificationException(e);
        // }
        // }
        // });
        // 重置按钮
        btnReset.setIcon(VaadinIcons.ROTATE_LEFT);
        btnReset.addStyleNames(MaterialTheme.BUTTON_ICON_ONLY);
        btnReset.addClickListener(this);

        // 更多按钮
        btnMore.setDescription(I18NUtility.getValue("Common.More", "More"));
        btnMore.addStyleName(MaterialTheme.BUTTON_ICON_ONLY);
        btnMore.setIcon(VaadinIcons.ELLIPSIS_CIRCLE_O);
        btnMore.addClickListener(this);
        hlSearchBar.addComponents(hlSearch, btnReset, btnMore);
        this.setCompositionRoot(hlSearchBar);
        this.setSizeUndefined();

        KeyAction ka = new KeyAction(KeyCode.ENTER);
        ka.addKeypressListener(new KeyActionListener() {

            /**
             *
             */
            private static final long serialVersionUID = -1386245490583258561L;

            @Override
            public void keyPressed(KeyActionEvent keyPressEvent) {
                popupExtension.close();
                search();
            }
        });
        ka.setPreventDefault(true);
        ka.setStopPropagation(true);
        ka.extend((AbstractComponent)view);
    }

    private void initPopupPanel() {
        // 更多搜索条件的弹出框
        popupExtension = PopupExtension.extend(btnMore);
        popupExtension.setAnchor(Alignment.BOTTOM_LEFT);
        popupExtension.setDirection(Alignment.BOTTOM_LEFT);
        popupExtension.setPopupStyleName(CoreTheme.SEARCH_PANEL_POPUP);
        // popupExtension.setCloseOnOutsideMouseClick(true);

        // 弹出框的内容
        VerticalLayout vlPopupContent = new VerticalLayout();
        vlPopupContent.setSizeUndefined();
        vlPopupContent.setSpacing(false);

        Button btnClose = new Button();
        btnClose.setDescription(I18NUtility.getValue("Common.Close", "Close"));
        btnClose.setIcon(VaadinIcons.CLOSE);
        btnClose.addStyleNames(ValoTheme.BUTTON_ICON_ONLY, ValoTheme.BUTTON_SMALL);
        btnClose.addClickListener(event -> {
            popupExtension.close();
        });
        AbstractLayout layout = condition.getLayout();
        vlPopupContent.addComponent(layout);
        vlPopupContent.setExpandRatio(layout, 1);

        HorizontalLayout hlBtn = new HorizontalLayout();
        hlBtn.setSizeUndefined();
        hlBtn.addStyleName(CoreTheme.BORDER_TOP);
        Button btnClear = new Button();
        btnClear.setDescription(I18NUtility.getValue("Common.Reset", "Reset"));
        btnClear.setIcon(VaadinIcons.ROTATE_LEFT);
        btnClear.addStyleNames(ValoTheme.BUTTON_ICON_ONLY, ValoTheme.BUTTON_SMALL);
        btnClear.addClickListener(event -> {
            condition.reset();
        });

        Button btnOk = new Button();
        btnOk.setTabIndex(2);
        btnOk.setDescription(I18NUtility.getValue("Common.Search", "Search"));
        btnOk.setIcon(VaadinIcons.SEARCH);
        btnOk.addStyleNames(ValoTheme.BUTTON_PRIMARY, ValoTheme.BUTTON_ICON_ONLY, ValoTheme.BUTTON_SMALL);
        btnOk.addClickListener(event -> {
            popupExtension.close();
            search();
        });
        // btnOk.setClickShortcut(KeyCode.ENTER);

        btnClear.setId("btn_dialog_clear");
        btnClose.setId("btn_dialog_close");
        btnOk.setId("btn_dialog_search");
        hlBtn.addComponents(btnClear, btnClose, btnOk);
        vlPopupContent.addComponent(hlBtn);
        vlPopupContent.setComponentAlignment(hlBtn, Alignment.BOTTOM_RIGHT);
        popupExtension.setContent(vlPopupContent);

        KeyAction ka = new KeyAction(KeyCode.ENTER);
        ka.addKeypressListener(new KeyActionListener() {

            /**
             *
             */
            private static final long serialVersionUID = -1386245490583258561L;

            @Override
            public void keyPressed(KeyActionEvent keyPressEvent) {
                popupExtension.close();
                search();
            }
        });
        ka.setPreventDefault(true);
        ka.setStopPropagation(true);
        ka.extend(vlPopupContent);
    }

    public IObjectListGrid<?> getObjectListGrid() {
        return objectListGrid;
    }

    @Override
    public void buttonClick(ClickEvent event) {
        if (event.getButton().equals(btnReset)) {
            condition.reset();
        } else if (event.getButton().equals(btnSearch)) {
            search();
        } else if (event.getButton().equals(btnMore)) {
            popupExtension.open();
            Iterator<Component> iterator = condition.getLayout().iterator();
            if(iterator.hasNext()){
                Component next =iterator().next();
                if (next instanceof Focusable) {
                    ((Focusable)next).focus();
                }
            }

        }
    }

    private void search() {
        if (this.objectListGrid != null) {
            this.objectListGrid.setFilter(condition.getFilter());
            this.objectListGrid.setStartPage(1, 0);
            this.objectListGrid.refresh();
        }

        if (this.view != null) {
            this.view.updateAfterFilterApply();
        }
    }
}

16.11.3. 创建SearchPanelBuilder

如果想要创建一个SearchPanel并添加至页面,只需new 一个SearchPanelBuilder,然后将其作为组件添加至容器中即可。

以Operation对象为例,参阅以下代码:

请注意Conditions的引入方式。

SearchPanelBuilder searchPanel = new SearchPanelBuilder(BeanManager.getService(OperationConditions.class), objectGrid, this);

hlToolBox.addComponent(searchPanel);

hlToolBox.setComponentAlignment(searchPanel, Alignment.MIDDLE_RIGHT);

16.12. 图片编辑组件

该组件与RoundImage组件相似,RoundImage组件一般是放在添加dialog中上传一张图片的显示组件,而图片编辑组件本质就是一个dialog, 内部定义了一个 ResponsiveRow 和 List<Image> ,它可以显示多张图片,具体效果如下:

picture edit

如何使用该组合件

    //注入图片编辑组件
    @Inject
    private IPictureEdit pictureEdit;

    private ResponsiveRow addRow;
    //编辑按钮点击事件
   if (button.equals(btnEdit)) {
        Iterator<Component> iterator = addRow.iterator();
        List<Image> images = new ArrayList<>();
        while (iterator.hasNext()) {
            ResponsiveColumn next = (ResponsiveColumn)iterator.next();
            images.add((Image)next.getComponent());
        }
        pictureEdit.setPictures(images);
        pictureEdit.show(this.getUI(), new DialogCallBack() {

            @Override
            public void done(ConfirmResult result) {
                if (result.getResult().equals(ConfirmResult.Result.OK)) {
                    List<Image> newImages = (List<Image>)result.getObj();
                    presenter.editUnitStationImage(wip, newImages);
                    loadImages(wip);
                }
            }
        });
   }

    private void loadImages(WIP<?> wip) {
           addRow.removeAllComponents();
           if (wip == null) {
               return;
           }
            //获取需要展示的图片集合
           List<ObjectStationImage> unitStationImages = presenter.getWIPStationImages(wip);

           for (ObjectStationImage image : unitStationImages) {
               Image c = new Image(null, new StreamResource(new StreamSource() {
                   private static final long serialVersionUID = -6831297225306728042L;

                   @Override
                   public InputStream getStream() {
                       try {
                           return image.getMedia().getMediaStream();
                       } catch (Exception e) {
                           return null;
                       }

                   }
               }, ""));

               c.setData(image.getId());
               c.setSizeFull();
               try {
                   image.getMedia().getMediaStream();
                   addRow.addColumn().withDisplayRules(12, 12, 4, 4).withComponent(c);
               } catch (Exception e) {
                   NotificationUtils.notificationError(e.getMessage() + ":" + image.getMedia().getFileURL());
               }

           }
    }

16.13. 图片预览组件

该组件的用处是上传的图片进行全屏查看,具体用法如下:

    //注入组件
    @Inject
    private IPicturePreview picturePreview;
    //预览按钮点击事件
    if (button.equals(btnPreview)) {
        WorkStation workStation = presenter.getWorkStation();
        if (wip == null || workStation == null) {
            NotificationUtils.notificationError(
                I18NUtility.getValue("WorkStation.UnitOrWorkStationNotFound", "Unit or WorkStation not found!"));
            return;
        }
        List<ObjectStationImage> images = presenter.getWIPStationImages(wip);
        InputStream[] inputStreams = new InputStream[images.size()];

        for (int i = 0; i < inputStreams.length; i++) {
            InputStream stream = null;
            try {
                stream = images.get(i).getMedia().getMediaStream();
            } catch (Exception e) {
                stream = null;
            }
            if (stream != null) {
                inputStreams[i] = stream;
            }
        }
        picturePreview.setInputStreams(inputStreams);
        picturePreview.show(getUI());
    }

16.14. 时间选择组件(TimeSelector)

页面具体使用和展示效果

timeselector

内部代码实现:

先创建private TimeSelector tsStartTime = new TimeSelector()对象,然后就和普通组件一样绑定值和获取值。

16.15. 文件上传

支持图片、文本的上传

16.16. TreeGridMultipleSelectionListener

16.17. 地区选择组件(SelectRegionBudilder)

该组件用于选择地区,如果想要创建一个SelectRegionBudilder并添加至页面,只需new 一个SelectRegionBudilder,然后将其中的三个ComboBox组件拿出添加至容器中即可。具体方法如下:

 private SelectRegionBuilder select = new SelectRegionBuilder();
 ComboBox<String> cbCountry = select.getCbCountry();
 ComboBox<String> cbCbState = select.getCbState();
 ComboBox<String> cbCbCity = select.getCbCity();

由于涉及到国际化显示的问题,在预览或编辑时,不能将数据库的值直接set进去,应通过SelectRegionBudilder组件提供的方法,进行国际化的转换。具体方法如下:

 cbCountry.setValue(select.getCountryLocale(account.getCountry()));
 cbCbState.setValue(select.getSateLocale(account.getCountry(), account.getState()));
 cbCbCity.setValue(select.getCityLocale(account.getCountry(), account.getState(), account.getCity()));
 cbCbCity.setValue(select.getCityLocale(account.getCountry(), account.getCity()));

国家、省(州)、市分别通过getCountryLocale、getSateLocale、getCityLocale进行转换。 特别注意考虑到有些国家没有State一级,所以城市的转换有两种情况,在setValue时请做好判断,排除空指针。

17. 系统时间处理

数据库获取的数据中时间字段的处理

用户每次登录时都会重新设置Session和Principals,请求页面时在RequestInfoFilter中为RequestInfo设置从Session中获取用户Locale和ZoneId; 后台从数据库获取数据采用hql语句时,在ObjectBaseImpl中将对象的公共时间属性(创建时间、修改时间、删除时间)已经做好处理

    //公共属性
    @Override
    public ZonedDateTime getCreateTime() {
         return getInternalObject().getCreateTime().withZoneSameInstant(RequestInfo.current()
            .getUserZoneId());
    }

    gridOrder.addColumn(new ValueProvider<WorkOrder, String>() {
        private static final long serialVersionUID = 1L;

        @Override
        public String apply(WorkOrder source) {
            //日期格式转换
            return source.getCreateTime() != null
                ? source.getCreateTime().format(DateTimeFormatter.ofPattern(DateTimeUtils.DEFAULT_DATETIME))
                : null;
        }
    });

   //非公共属性,先要转换成用户对应的时区
    gridOrder.addColumn(new ValueProvider<WorkOrder, String>() {
        private static final long serialVersionUID = 1L;

        @Override
        public String apply(WorkOrder source) {
            return source.getClosedTime() != null
                ? source.getClosedTime().withZoneSameInstant(RequestInfo.current().getUserZoneId())
                    .format(DateTimeFormatter.ofPattern(DateTimeUtils.DEFAULT_DATETIME))
                : null;
        }
    });

如果采用sql语句查询出来的数据,在传到前端时一定要将时间有关的数据做处理,否则页面显示就不正确

    Timestamp start = (Timestamp)temp[1];
    wrapper.setStartTime(start == null ? "" :DateTimeUtils.format(start,RequestInfo.current().getUserZoneId()));
数据库的时区要和应用的时区一致
DateTimeUtils中提供了三种日期格式和多种时间转换的方法,根据实际情况使用
DateTimeUtils源代码
package com.ags.lumosframework.common.util;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.Objects;

public class DateTimeUtils {

    public static final String DEFAULT_DATETIME = "yyyy-MM-dd HH:mm:ss";

    public final static String FormatterDefineOracle = "yyyy-mm-dd hh24:mi:ss";

    public final static String DEFAULT_DATE = "yyyy-MM-dd";


    public static String format(ZonedDateTime zonedDateTime) {
        return format(zonedDateTime, ZoneId.systemDefault(), DateTimeFormatter.ofPattern(DEFAULT_DATETIME));
    }

    public static String format(ZonedDateTime zonedDateTime, ZoneId zoneId) {
        return format(zonedDateTime, zoneId, DateTimeFormatter.ofPattern(DEFAULT_DATETIME));
    }

    public static String format(ZonedDateTime zonedDateTime, String format) {
        return format(zonedDateTime, ZoneId.systemDefault(), DateTimeFormatter.ofPattern(format));
    }

    public static String format(ZonedDateTime zonedDateTime, ZoneId zoneId, String format) {
        return format(zonedDateTime, zoneId, DateTimeFormatter.ofPattern(format));
    }

    public static String format(Date date, ZoneId zoneId) {
        return date.toInstant().atZone(zoneId).format(DateTimeFormatter.ofPattern(DEFAULT_DATETIME));
    }

    public static String format(ZonedDateTime zonedDateTime, ZoneId zoneId, DateTimeFormatter dateTimeFormatter) {
        return Objects.isNull(zonedDateTime) ? "" : zonedDateTime.withZoneSameInstant(zoneId).format(dateTimeFormatter);
    }

    public static Date getDate(LocalDate localDate) {
        return getDate(localDate, ZoneId.systemDefault());
    }

    public static Date getDate(LocalDate localDate, ZoneId zoneId) {
        return Objects.isNull(localDate) ? null : Date.from(localDate.atStartOfDay().atZone(zoneId).toInstant());
    }

    public static Date getDate(LocalDateTime localDateTime) {
        return getDate(localDateTime, ZoneId.systemDefault());
    }

    public static Date getDate(LocalDateTime localDateTime, ZoneId zoneId) {
        return Objects.isNull(localDateTime) ? null : Date.from(localDateTime.atZone(zoneId).toInstant());
    }

    public static Date getDate(ZonedDateTime zonedDateTime) {
        return Objects.isNull(zonedDateTime) ? null : Date.from(zonedDateTime.toInstant());
    }

    public static LocalDate getLocalDate(Date date) {
        return getLocalDate(date, ZoneId.systemDefault());
    }

    public static LocalDate getLocalDate(Date date, ZoneId zoneId) {
        return Objects.isNull(date) ? null : date.toInstant().atZone(zoneId).toLocalDate();
    }

    public static LocalDateTime getLocalDateTime(Date date) {
        return getLocalDateTime(date, ZoneId.systemDefault());
    }

    public static LocalDateTime getLocalDateTime(Date date, ZoneId zoneId) {
        return Objects.isNull(date) ? null : LocalDateTime.ofInstant(date.toInstant(), zoneId);
    }

    public static ZonedDateTime getZonedDateTime(Date date) {
        return getZonedDateTime(date, ZoneId.systemDefault());
    }

    public static ZonedDateTime getZonedDateTime(Date date, ZoneId zoneId) {
        return Objects.isNull(date) ? null : ZonedDateTime.ofInstant(date.toInstant(), zoneId);
    }

}

18. vaadin themes

18.1. 概述

自定义主题文件位于Web应用程序的 VAADIN/themes

主题目录结构图如下图所示(图片来自官网):

theme contents
valo为vaddin官方默认的内置主题,内置主题存储在 vaadin-themes.jar中

18.2. Material Theme

lumos-material 项目是一个独立的项目,它提供了一套基础样式库。您可以根据自己的设计随意修改里面的scss文件。

18.2.1. 简介

Material主题位于 lumos-material 项目的 VAADIN/themes/ 文件夹下。

Material主题是Google Material Design规范的实现。我们在此基础上修改了一些组件的样式,并添加了一些自定义的样式。

默认情况下,所有Vaadin组件都实现了valo主题,所以Material主题也是以Valo为基本主题。

目录结构
material theme

18.2.2. 主题文件

styles.scss文件中包含material主题文件(即material.scss),如下所示:

@import "material.scss";
.material {
    @include material;
}

material.scss文件中包含了所有组件及自定义等样式的引用,参数的定义(variables.scss)需要在@import语句之前引用。如下所示:

material scss

18.2.3. 参数定义

valo中内置参数

一些常规的定义:

$v-focus-color: #2196F3 !default; --重点色
$v-background-color:#fafafa; --Valo主题的主要控制参数,它用于计算主题中的所有其他颜色
$v-app-background-color:#fafafa --UI的根元素的背景颜色。
$v-app-loading-text:“Loading Resources…​” --在加载和启动客户端引擎时,加载时显示的静态文本。
$v-line-height:1.6; --组件的高度,它以小数为单位制定。
$v-font-size:14px; --基本字体大小,它以像素为单位指定。
$v-font-color:14px; --基本字体大小,它以像素为单位指定。
$v-font-family: "Source Sans Pro", sans-serif !default; --基本字体。
$v-unit-size: 32px !default; --这是各种布局单元的基本大小,它直接用于某些控件的大小,例如按钮高度和Layout的边距。

有关特定于组件的样式的完整最新列表,请参阅ValoTheme中的Vaadin API文档。 Valo Theme 官方文档

Lumos中自定义参数

$jasper-background-color: #ecf0f5 !default; --主要用于view的背景色
$jasper-header-color: #273644 !default; --header.scss中使用,用于定义header的背景色
$jasper-header-height:50px !default; --在header.scss中使用,用于定义header的高度
$jasper-border-color: rgba(0,0,0,0.1); --边框的颜色,主要用于button及input等组件
$jasper-icon-size:12px; --图标的大小

18.2.4. 常量定义

与valo内置主题相关的各种常量在 com.vaadin.ui.themes 包中的 ValoTheme 类中定义,这些主要是特定组件的特殊样式名称。

与material主题相关的各种常量在 com.github.appreciated.material 包中的 MaterTheme 类中定义。

与lumos主题相关的各种常量在 com.ags.lumosframework.web.vaadin.base 包中的 CoreTheme 类中定义。

//定义一个按钮,并为按钮添加 “只有图标”和 “圆角”和“背景为黄色”的样式
Button btn = new Button ("Button");
btn.addStyleNames(ValoTheme.BUTTON_ICON_ONLY, MaterialTheme.BUTTON_ROUND, CoreTheme.BACKGROUND_YELLOW);

18.3. 创建和使用自定义主题

自定义主题会被放置在Web应用程序的 VAADIN/themes 中,如果是maven项目,将位于 src/main/webapp 下,如果是Eclipse项目,将位于 WebContent 下,并且这个位置是固定的。另外,每创建一个主题都需要有一个主题文件夹。

1、主题需要在基础主题上进行扩展,不建议直接复制和修改基础主题。
2、主题文件夹的名称决定了主题的名称,名称用于@Theme注解。
3、一个自定义主题必须包含一个styles.scss文件,其他文件可以自由命名。
4、建议至少在两个SCSS文件中组织主题。
5、不需要手动添加 styles.css 文件,当浏览器请求theme时,此文件会在scss文件编译时自动生成。

18.3.1. 创建自定义主题

以创建一个名为 mytheme 的主题为例:

1、在 VAADIN/themes 下创建一个名为 mytheme 的文件夹。
2、在 mytheme 文件夹下创建一个 styles.scss 文件来定义主题的规则。
styles.scss 中,建议将定义的规则以主题名称为选择器,如:将规则都包含在 .mytheme

@import "addons.scss";
@import "mytheme.scss";

.mytheme {
  @include addons;
  @include mytheme;
}

3、在 mytheme 文件夹下创建一个 mytheme.scss 作为实际的主题文件。实际主题定义如下:

实际主题作为mixin,并包含了基本主题material。

@import "../material/material.scss";

@mixin mytheme {
  @include material;

  /* An actual theme rule */
  .v-button {
    color: blue;
  }
}
建议主题文件使用实际的主题名来作为前缀,例如:mytheme.scss,dark.scss。
add-on theme

如果使用了包含scss的vaadin插件,当引入这些插件时,Eclipse的Vaadin插件或Maven会将其对应的规则自动添加到addons.scss文件中,并被包含在styles.scss中。该addons.scss文件由Eclipse的Vaadin插件或Maven自动生成,不需要手动添加。

18.3.2. 主题应用

可以通过为应用程序的UI类使用@Theme注解来指定主题,如下所示:

@Theme("mytheme")
public class MyUI extends UI {
    @Override
    protected void init(VaadinRequest request) {
        ...
    }
}
样式标准组件

Vaadin中的每个组件都有一个CSS样式类,您可以使用它来控制组件的样式。可以使用addStyleName()添加定义好的样式名。

Button smallButton = new Button ("Small Valo Button");
smallButton.addStyleName(ValoTheme.BUTTON_SMALL);
getStyleName() 仅返回自定义样式名,而不返回组件的内置样式名。

有关其样式的说明,请参阅每个组件的介绍。

18.3.3. Lumos主题

Lumos提供了两种自定义主题:light(浅色)和dark(深色),主题文件位于 lumos-vaadin 项目的 VAADIN/themes/ 文件夹下。

light

此主题基于material主题,基本没做修改。

dark

此主题基于material主题,对背景色等参数做了修改,详见dark主题下的 variables.scss

18.3.4. 如何切换Theme

如何将自定义主题应用到整个应用程序或部分应用,以dark主题为例:

1.永久使用dark主题

在UI中使用 @Theme 注解,这是最简单也是最推荐的方法。

@Theme("dark")
public class MainUI extends UI {
  //...
}

2.在light和dark中自由切换

在页面上提供一个按钮或者单选按钮,来让用户决定使用哪种主题。如:

public class MainUI extends UI {

  public MainUI() {
    RadioButtonGroup<VaadinTheme> themeRbg = new RadioButtonGroup<VaadinTheme>();
    themeRbg.addValueChangeListener(new ValueChangeListener<VaadinTheme>() {
            @Override
            public void valueChange(ValueChangeEvent<VaadinTheme> event) {
                String selected = event.getValue().getName();
                UI currentUI = UI.getCurrent();
                if (currentUI != null) {
                    currentUI.setTheme(selected);
                }
            }
        });
  }
}

18.4. Scss简介

Vaadin使用Scss作为样式表。Scss是CSS3的扩展,它为CSS添加了嵌套规则、变量、mixins、选择器继承和其他功能。

18.4.1. 变量

Scss允许定义可以在规则中使用的变量。

$textcolor: blue;

.v-button-caption {
  color: $textcolor;
}

上面的规则将编译为CSS:

.v-button-caption {
  color: blue;
}

18.4.2. 嵌套

Scss支持嵌套规则,这些规则被编译为内部选择器。例如:

.v-app {
  background: yellow;

  .mybutton {
    font-style: italic;
    .v-button-caption {
      color: blue;
    }
  }
}

编译为:

.v-app {
  background: yellow;
}

.v-app .mybutton {
    font-style: italic;
}

.v-app .mybutton .v-button-caption {
  color: blue;
}

18.4.3. Mixins

Mixins是可以被包含在其他规则中的规则。
你可以通过在规则名称前面添加@mixin关键字来定义mixin规则,然后使用@include将其应用于其他规则,同时还可以向其传递参数,这些参数在mixin中作为局部变量处理。
例如:

@mixin mymixin {
  background: yellow;
}

@mixin marginmixin($param) {
  margin: $param;
}

.v-button-caption {
  @include mymixin;
  @include marginmixin(10px);
}

上面的规则将编译为如下的CSS:

.v-button-caption {
  background: yellow;
  margin: 10px;
}

18.4.4. 编译Scss

必须将Sass主题编译为浏览器理解的CSS,可以使用Vaadin Sass编译器进行编译,也可以在Eclipse、Maven中编译,也可以在浏览器中加载应用程序时即时运行。

有关Sass的相关文档请参阅: Sass 官方文档