在本教程中,我们将遵循开发插件  来开发我们的JDBC Form Binder插件的  指导原则。另请参阅第一个教程  如何开发一个Bean Shell Hash变量插件  以及以下与JDBC相关的插件以获取更多详细信息步骤。

1.什么问题?

为了集成的目的,我们希望将表单数据存储到不同的数据库表,而不是Joget表单数据表。

2.你有什么想法来解决这个问题?

Joget Workflow提供了一个名为Form Store Binder Plugin的插件类型  。我们将开发一个支持JDBC连接和自定义查询来存储表单数据。

3.你的插件需要什么输入?

要开发一个JDBC Store绑定器,我们将需要JDBC连接设置以及自定义查询来存储基于收集的表单数据的表单数据。

  1. 数据源:使用自定义数据源或Joget默认数据源
  2. 自定义JDBC驱动程序:自定义数据源的JDBC驱动程序
  3. 自定义JDBC URL:自定义数据源的JDBC连接URL
  4. 自定义JDBC用户名:自定义数据源的用户名
  5. 自定义JDBC密码:自定义数据源的密码
  6. SQL Check Exist Query:  查询是否应执行插入或更新查询。
  7. SQL插入查询:插入表单数据的查询。 
  8. SQL更新查询:插入表单数据的查询。 
  9. SQL删除查询:作为多行文件夹使用时删除已删除表格数据的查询。

我们将不得不支持将表单数据注入查询的语法。“{foreignKey}”可用于多行存储。

我们还需要支持注入UUID值的语法。在这种情况下,我们将使用“{uuid}”。

 Example: INSERT INTO app_fd_test VALUES ({id}, {name}, {email}, {phone}, {foreignKey});

4.你的插件的输出和预期结果是什么?

所有提交的数据将根据 检查/插入/更新 查询进行相应的存储。

5.有没有可重用的资源/ API?

我们可以参考其他可用的  Form Store活页夹插件的实现。可以使用AppUtil.getApplicationContext().getBean("setupDataSource").来检索Joget的默认数据源。

6.准备你的开发环境

我们需要始终准备好Joget Workflow Source Code,并按照这个指导方针建立起来  。 

本教程的以下内容是使用Macbook Pro和Joget源代码5.0.0版编写的。 其他平台命令请参考  如何开发插件

让我们说我们的文件夹目录如下。 

- Home
  - joget
    - plugins
    - jw-community
      -5.0.0

“plugins”目录是我们要创建和存储我们所有插件的文件夹,“jw-community”目录是Joget Workflow源代码存储的地方。

运行以下命令在“plugins”目录下创建一个maven项目。

cd joget/plugins/
~/joget/jw-community/5.0.0/wflow-plugin-archetype/create-plugin.sh org.joget.tutorial jdbc_store_binder 5.0.0

然后,shell脚本将要求我们为您的插件输入一个版本,并在生成maven项目之前要求我们确认。

Define value for property 'version':  1.0-SNAPSHOT: : 5.0.0
[INFO] Using property: package = org.joget.tutorial
Confirm properties configuration:
groupId: org.joget.tutorial
artifactId: jdbc_store_binder
version: 5.0.0
package: org.joget.tutorial
Y: : y

我们应该在终端上显示“BUILD SUCCESS”消息,在“plugins”文件夹中创建一个“jdbc_store_binder”文件夹。

用你喜欢的IDE打开maven项目。我将使用  NetBeans。  

7. Just code it!

a. 扩展插件类型的抽象类

 

在“org.joget.tutorial”包下创建一个“JdbcStoreBinder”类。然后,使用org.joget.apps.form.model.FormBinder  抽象类来扩展  该类。

为了使它作为一个表单存储区活页夹,我们将需要实现  org.joget.apps.form.model.FormStoreBinder  接口。

然后,我们需要实现  org.joget.apps.form.model.FormStoreElementBinder  接口来使这个插件显示为一个选择,在store binder选择框中实现  org.joget.apps.form.model.FormStoreMultiRowElementBinder  接口,将其列在存储活页元素的选择框。 

 

 请参阅 Form Store Binder Plugin. 

b. 实现所有的抽象方法

像往常一样,我们必须执行所有的抽象方法。我们将使用AppPluginUtil.getMessage方法来支持i18n,并使用常量变量MESSAGE_PATH作为消息资源包目录。

 

package org.joget.tutorial;
 
import org.joget.apps.app.service.AppPluginUtil;
import org.joget.apps.app.service.AppUtil;
import org.joget.apps.form.model.Element;
import org.joget.apps.form.model.FormBinder;
import org.joget.apps.form.model.FormData;
import org.joget.apps.form.model.FormRowSet;
import org.joget.apps.form.model.FormStoreBinder;
import org.joget.apps.form.model.FormStoreElementBinder;
import org.joget.apps.form.model.FormStoreMultiRowElementBinder;
 
public class JdbcStoreBinder extends FormBinder implements FormStoreBinder, FormStoreElementBinder, FormStoreMultiRowElementBinder {
    
    private final static String MESSAGE_PATH = "messages/JdbcStoreBinder";
    
    public String getName() {
        return "JDBC Store Binder";
    }
 
    public String getVersion() {
        return "5.0.0";
    }
    
    public String getClassName() {
        return getClass().getName();
    }
 
    public String getLabel() {
        //support i18n
        return AppPluginUtil.getMessage("org.joget.tutorial.JdbcStoreBinder.pluginLabel", getClassName(), MESSAGE_PATH);
    }
    
    public String getDescription() {
        //support i18n
        return AppPluginUtil.getMessage("org.joget.tutorial.JdbcStoreBinder.pluginDesc", getClassName(), MESSAGE_PATH);
    }
 
    public String getPropertyOptions() {
        return AppUtil.readPluginResource(getClassName(), "/properties/jdbcStoreBinder.json", null, true, MESSAGE_PATH);
    }
 
    public FormRowSet store(Element element, FormRowSet rows, FormData formData) {
        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
    }
}

然后,我们必须为管理员用户提供一个UI来为我们的插件提供输入。在getPropertyOptions方法中,我们已经指定了我们的  插件属性选项和配置  定义文件位于“/properties/jdbcStoreBinder.json”。让我们在“jdbc_store_binder / src / main”目录下创建一个目录“resources / properties”。创建目录后,在“properties”文件夹中创建一个名为“jdbcStoreBinder.json”的文件。

在属性定义选项文件中,我们需要提供如下的选项。请注意,我们可以在我们的属性选项中使用“@@ message.key @@”语法来支持i18n。

[{
    title : '@@form.jdbcStoreBinder.config@@',
    properties : [{
        name : 'jdbcDatasource',
        label : '@@form.jdbcStoreBinder.datasource@@',
        type : 'selectbox',
        options : [{
            value : 'custom',
            label : '@@form.jdbcStoreBinder.customDatasource@@'
        },{
            value : 'default',
            label : '@@form.jdbcStoreBinder.defaultDatasource@@'
        }],
        value : 'default'
    },{
        name : 'jdbcDriver',
        label : '@@form.jdbcStoreBinder.driver@@',
        description : '@@form.jdbcStoreBinder.driver.desc@@',
        type : 'textfield',
        value : 'com.mysql.jdbc.Driver',
        control_field: 'jdbcDatasource',
        control_value: 'custom',
        control_use_regex: 'false',
        required : 'true'
    },{
        name : 'jdbcUrl',
        label : '@@form.jdbcStoreBinder.url@@',
        type : 'textfield',
        value : 'jdbc:mysql://localhost/jwdb?characterEncoding=UTF8',
        control_field: 'jdbcDatasource',
        control_value: 'custom',
        control_use_regex: 'false',
        required : 'true'
    },{
        name : 'jdbcUser',
        label : '@@form.jdbcStoreBinder.username@@',
        type : 'textfield',
        control_field: 'jdbcDatasource',
        control_value: 'custom',
        control_use_regex: 'false',
        value : 'root',
        required : 'true'
    },{
        name : 'jdbcPassword',
        label : '@@form.jdbcStoreBinder.password@@',
        type : 'password',
        control_field: 'jdbcDatasource',
        control_value: 'custom',
        control_use_regex: 'false',
        value : ''
    },{
        name : 'check_sql',
        label : '@@form.jdbcStoreBinder.check_sql@@',
        description : '@@form.jdbcStoreBinder.check_sql.desc@@',
        type : 'codeeditor',
        mode : 'sql',
        required : 'true'
    },{
        name : 'insert_sql',
        label : '@@form.jdbcStoreBinder.insert_sql@@',
        description : '@@form.jdbcStoreBinder.insert_sql.desc@@',
        type : 'codeeditor',
        mode : 'sql',
        required : 'true'
    },{
        name : 'update_sql',
        label : '@@form.jdbcStoreBinder.update_sql@@',
        description : '@@form.jdbcStoreBinder.update_sql.desc@@',
        type : 'codeeditor',
        mode : 'sql',
        required : 'true'
    },{
        name : 'delete_sql',
        label : '@@form.jdbcStoreBinder.delete_sql@@',
        description : '@@form.jdbcStoreBinder.delete_sql.desc@@',
        type : 'codeeditor',
        mode : 'sql',
        required : 'true'
    }],
    buttons : [{
        name : 'testConnection',    
        label : '@@form.jdbcStoreBinder.testConnection@@',
        ajax_url : '[CONTEXT_PATH]/web/json/app[APP_PATH]/plugin/org.joget.tutorial.JdbcStoreBinder/service?action=testConnection',
        fields : ['jdbcDriver', 'jdbcUrl', 'jdbcUser', 'jdbcPassword'],
        control_field: 'jdbcDatasource',
        control_value: 'custom',
        control_use_regex: 'false'
    }]
}]

JDBC Options Binder相同  ,我们需要添加一个测试连接按钮来进行自定义的JDBC设置。请参阅  如何开发一个JDBC选项绑定器  上的  Web服务插件  实现。

一旦我们完成了收集输入的属性选项和Web服务来测试连接,我们可以使用插件的主要方法是存储方法。

    public FormRowSet store(Element element, FormRowSet rows, FormData formData) {
        Form parentForm = FormUtil.findRootForm(element);
        String primaryKeyValue = parentForm.getPrimaryKeyValue(formData);
            
        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        
        try {
            DataSource ds = createDataSource();
            con = ds.getConnection();
            
            //check for deletion
            FormRowSet originalRowSet = formData.getLoadBinderData(element);
            if (originalRowSet != null && !originalRowSet.isEmpty()) {
                for (FormRow r : originalRowSet) {
                    if (!rows.contains(r)) {
                        String query = getPropertyString("delete_sql");    
                        pstmt = con.prepareStatement(getQuery(query));
                        int i = 1;
                        for (String obj : getParams(query, r, primaryKeyValue)) {
                            pstmt.setObject(i, obj);
                            i++;
                        }
                        pstmt.executeUpdate();
                    }
                }
            }
            
            if (!(rows == null || rows.isEmpty())) {
            
                //run query for each row
                for (FormRow row : rows) {
                    //check to use insert query or update query
                    String checkSql = getPropertyString("check_sql");
                    pstmt = con.prepareStatement(getQuery(checkSql));
                    int i = 1;
                    for (String obj : getParams(checkSql, row, primaryKeyValue)) {
                        pstmt.setObject(i, obj);
                        i++;
                    }
                    String query = getPropertyString("insert_sql");
                    rs = pstmt.executeQuery();
                    //record exist, use update query
                    if (rs.next()) {
                        query = getPropertyString("update_sql");
                    }
                    pstmt = con.prepareStatement(getQuery(query));
                    i = 1;
                    for (String obj : getParams(query, row, primaryKeyValue)) {
                        pstmt.setObject(i, obj);
                        i++;
                    }
                    pstmt.executeUpdate();
                }
            }
        } catch (Exception e) {
            LogUtil.error(getClassName(), e, "");
        } finally {
            try {
                if (rs != null) {
                    rs.close();
                }
                if (pstmt != null) {
                    pstmt.close();
                }
                if (con != null) {
                    con.close();
                }
            } catch (Exception e) {
                LogUtil.error(getClassName(), e, "");
            }
        }
        
        return rows;
    }
    
    /**
     * Used to replaces all syntax like {field_id} to question mark
     * @param query
     * @return 
     */
    protected String getQuery(String query) {
        return query.replaceAll("\\{[a-zA-Z0-9_]+\\}", "?");
    }
    
    /**
     * Used to retrieves the value of variables in query 
     * @param query
     * @param row
     * @return 
     */
    protected Collection<String> getParams(String query, FormRow row, String primaryKey) {
        Collection<String> params = new ArrayList<String>();
        
        Pattern pattern = Pattern.compile("\\{([a-zA-Z0-9_]+)\\}");
        Matcher matcher = pattern.matcher(query);
        
        while (matcher.find()) {
            String key = matcher.group(1);
            
            if (FormUtil.PROPERTY_ID.equals(key)) {
                String value = row.getId();
                if (value == null || value.isEmpty()) {
                    value = UuidGenerator.getInstance().getUuid();
                    row.setId(value);
                }
                params.add(value);
            } else if ("uuid".equals(key)) {
                params.add(UuidGenerator.getInstance().getUuid());
            } else if ("foreignKey".equals(key)) {
                params.add(primaryKey);
            } else {
                String value = row.getProperty(key);
                params.add((value != null)?value:"");
            }
        }
        
        return params;
    }
    
    /**
     * To creates data source based on setting
     * @return
     * @throws Exception 
     */
    protected DataSource createDataSource() throws Exception {
        DataSource ds = null;
        String datasource = getPropertyString("jdbcDatasource");
        if ("default".equals(datasource)) {
            // use current datasource
            ds = (DataSource)AppUtil.getApplicationContext().getBean("setupDataSource");
        } else {
            // use custom datasource
            Properties dsProps = new Properties();
            dsProps.put("driverClassName", getPropertyString("jdbcDriver"));
            dsProps.put("url", getPropertyString("jdbcUrl"));
            dsProps.put("username", getPropertyString("jdbcUser"));
            dsProps.put("password", getPropertyString("jdbcPassword"));
            ds = BasicDataSourceFactory.createDataSource(dsProps);
        }
        return ds;
    }

c. 管理你的插件的依赖库

我们的插件使用dbcp,javax.servlet.http.HttpServletRequest和javax.servlet.http.HttpServletResponse类,所以我们需要将jsp-api和commons-dbcp库添加到我们的POM文件中。

<!-- Change plugin specific dependencies here -->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jsp-api</artifactId>
    <version>2.0</version>
</dependency>
<dependency>
    <groupId>commons-dbcp</groupId>
    <artifactId>commons-dbcp</artifactId>
    <version>1.3</version>
</dependency>
<!-- End change plugin specific dependencies here -->

d. 让你的插件国际化(i18n)准备就绪

我们在getLabel和getDescription方法中使用i18n消息密钥。我们还在我们的属性选项定义中使用了i18n消息密钥。所以,我们需要为我们的插件创建一个消息资源包属性文件。

在“jdbc_store_binder / src / main”目录下创建目录“resources / messages”。然后,在该文件夹中创建一个“JdbcStoreBinder.properties”文件。在属性文件中,让我们添加所有的消息键和它的标签如下。

org.joget.tutorial.JdbcStoreBinder.pluginLabel=JDBC Binder
org.joget.tutorial.JdbcStoreBinder.pluginDesc=Used to store form data using JDBC
form.jdbcStoreBinder.config=Configure JDBC Binder
form.jdbcStoreBinder.datasource=Datasource
form.jdbcStoreBinder.customDatasource=Custom Datasource
form.jdbcStoreBinder.defaultDatasource=Default Datasource
form.jdbcStoreBinder.driver=Custom JDBC Driver
form.jdbcStoreBinder.driver.desc=Eg. com.mysql.jdbc.Driver (MySQL), oracle.jdbc.driver.OracleDriver (Oracle), com.microsoft.sqlserver.jdbc.SQLServerDriver (Microsoft SQL Server)
form.jdbcStoreBinder.url=Custom JDBC URL
form.jdbcStoreBinder.username=Custom JDBC Username
form.jdbcStoreBinder.password=Custom JDBC Password
form.jdbcStoreBinder.check_sql=SQL SELECT Query
form.jdbcStoreBinder.check_sql.desc=Used  to decide an insert or update operation. Use syntax like {field_id} in query to inject submitted form data.
form.jdbcStoreBinder.insert_sql=SQL INSERT Query
form.jdbcStoreBinder.insert_sql.desc=Use syntax like {field_id} in query to inject submitted form data.
form.jdbcStoreBinder.update_sql=SQL UPDATE Query
form.jdbcStoreBinder.update_sql.desc=Use syntax like {field_id} in query to inject submitted form data.
form.jdbcStoreBinder.delete_sql=SQL DELETE Query
form.jdbcStoreBinder.delete_sql.desc=Used to delete deleted form data in Grid element. Use syntax like {id} in query to inject form data primary key.
form.jdbcStoreBinder.testConnection=Test Connection
form.jdbcStoreBinder.connectionOk=Database connected
form.jdbcStoreBinder.connectionFail=Not able to establish connection.

e. 在Felix框架注册你的插件

我们将不得不在Activator类(在同一个类包中自动生成)中注册我们的插件类,以告诉Felix框架这是一个插件。

    public void start(BundleContext context) {
        registrationList = new ArrayList<ServiceRegistration>();
        //Register plugin here
        registrationList.add(context.registerService(JdbcStoreBinder.class.getName(), new JdbcStoreBinder(), null));
    } 

f.构建和测试

让我们建立我们的插件。一旦构建过程完成,我们将在“jdbc_store_binder / target”目录下创建一个“jdbc_store_binder-5.0.0.jar”文件。

然后,让插件jar上传到  管理插件。上传jar文件后,仔细检查插件是否正确上传和激活。

让创建一个窗体来创建和更新用户到dir_user表。

然后,使用以下查询配置表单的存储联编程序。

select username from dir_user where username = {id}
insert into dir_user
(id, username, firstName, lastName, email, active)
values
({id}, {id}, {firstName}, {lastName}, {email}, 1)

注意:{uuid}可以用来生成一个唯一的ID

update dir_user set firstName = {firstName}, lastName = {lastName},
email = {email}
where username = {id}

 

delete from TABLE_NAME where id = {id}

现在,让我们测试一下添加用户

检查用户是否在dir_user表中创建。

让我们通过在URL参数中传递id来更新相同的记录。

检查用户是否更新。

It works! 请记住测试插件的其他功能。 (big grin)

8. Take a step further, share it or sell it

You can download the source code from jdbc_store_binder_src.zip

To download the ready-to-use plugin jar, please find it in http://marketplace.joget.org/.