在本教程中,我们将遵循开发插件  来开发我们的JDBC选项活页夹插件的  指导原则。请参阅第一个教程  如何开发Bean Shell Hash变量  以获取更多详细信息步骤。

1.什么问题?

有时,我们可能需要编写一些自定义查询来填充多选项字段的选项。

2.如何解决问题?

Joget Workflow提供了一个名为Form Options Binder Plugin的插件类型  。我们将开发一个支持JDBC连接和自定义查询。

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

要开发一个JDBC选项绑定器,我们将需要JDBC连接设置以及自定义查询来填充选项。

  1. 数据源:使用自定义数据源或Joget默认数据源
  2. 自定义JDBC驱动程序:自定义数据源的JDBC驱动程序
  3. 自定义JDBC URL:  自定义数据源的JDBC连接URL
  4. 自定义JDBC用户名:  自定义数据源的用户名
  5. 自定义JDBC密码:  自定义数据源的密码
  6. SQL查询:查询来填充选项。 
  7. 使用Ajax:复选框决定是否使用AJAX加载选项。(对于AJAX级联下拉列表

查询还应该支持在使用AJAX时注入依赖关系值的语法。

例:

  1. SELECT id, name from app_fd_sample where group = ?
  2. SELECT id, name from app_fd_sample where group in (?)

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

返回的JDBC结果的第一列将是选项的值,第二列是选项的标签。当不使用AJAX级联下拉列表时,会有另一个可选的第三列进行分组。

5.是否有任何资源/ API可以重复使用?

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

6.准备你的开发环境

我们需要始终准备好我们的Joget工作流源代码,并遵循  这个指导原则。 

以下教程是使用Macbook Pro和Joget源代码5.0.0版编写的。请参阅 为其他平台命令开发插件文章的  指南 

假设我们的文件夹目录如下所示。 

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

“插件”目录是我们将创建和存储我们所有插件的文件夹,“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_options_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_options_binder
version: 5.0.0
package: org.joget.tutorial
Y: : y

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

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

7. 开始编码吧!

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

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

为了使其作为Form Options Binder工作,我们需要实现org.joget.apps.form.model.FormLoadOptionsBinder接口。我们也想支持AJAX Cascading Drop-Down List  ,所以我们还需要实现  org.joget.apps.form.model.FormAjaxOptionsBinder  接口。 

请参阅 Form Options Binder Plugin.

b. 实现所有的抽象方法

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

Implementation of all basic abstract methods
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.FormAjaxOptionsBinder;
import org.joget.apps.form.model.FormBinder;
import org.joget.apps.form.model.FormData;
import org.joget.apps.form.model.FormLoadOptionsBinder;
import org.joget.apps.form.model.FormRowSet;
 
public class JdbcOptionsBinder extends FormBinder implements FormLoadOptionsBinder, FormAjaxOptionsBinder {
    
    private final static String MESSAGE_PATH = "messages/JdbcOptionsBinder";
    
    public String getName() {
        return "JDBC Option 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.JdbcOptionsBinder.pluginLabel", getClassName(), MESSAGE_PATH);
    }
    
    public String getDescription() {
        //support i18n
        return AppPluginUtil.getMessage("org.joget.tutorial.JdbcOptionsBinder.pluginDesc", getClassName(), MESSAGE_PATH);
    }
 
    public String getPropertyOptions() {
        return AppUtil.readPluginResource(getClassName(), "/properties/jdbcOptionsBinder.json", null, true, MESSAGE_PATH);
    }

    public FormRowSet load(Element element, String primaryKey, FormData formData) {
        return loadAjaxOptions(null); // reuse loadAjaxOptions method
    }
 
    public boolean useAjax() {
        return "true".equalsIgnoreCase(getPropertyString("useAjax")); // let user to decide whether or not to use ajax for dependency field
    }
 
    public FormRowSet loadAjaxOptions(String[] dependencyValues) {
        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
    }
}

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

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

[{
    title : '@@form.jdbcOptionsBinder.config@@',
    properties : [{
        name : 'jdbcDatasource',
        label : '@@form.jdbcOptionsBinder.datasource@@',
        type : 'selectbox',
        options : [{
            value : 'custom',
            label : '@@form.jdbcOptionsBinder.customDatasource@@'
        },{
            value : 'default',
            label : '@@form.jdbcOptionsBinder.defaultDatasource@@'
        }],
        value : 'default'
    },{
        name : 'jdbcDriver',
        label : '@@form.jdbcOptionsBinder.driver@@',
        description : '@@form.jdbcOptionsBinder.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.jdbcOptionsBinder.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.jdbcOptionsBinder.username@@',
        type : 'textfield',
        control_field: 'jdbcDatasource',
        control_value: 'custom',
        control_use_regex: 'false',
        value : 'root',
        required : 'true'
    },{
        name : 'jdbcPassword',
        label : '@@form.jdbcOptionsBinder.password@@',
        type : 'password',
        control_field: 'jdbcDatasource',
        control_value: 'custom',
        control_use_regex: 'false',
        value : ''
    },{
        name : 'useAjax',
        label : '@@form.jdbcOptionsBinder.useAjax@@',
        type : 'checkbox',
        options : [{
            value : 'true',
            label : ''
        }]
    },{
        name : 'addEmpty',
        label : '@@form.jdbcOptionsBinder.addEmpty@@',
        type : 'checkbox',
        options : [{
            value : 'true',
            label : ''
        }]
    },{
        name : 'emptyLabel',
        label : '@@form.jdbcOptionsBinder.emptyLabel@@',
        type : 'textfield',
        control_field: 'addEmpty',
        control_value: 'true',
        control_use_regex: 'false',
        value : ''
    },{
        name : 'sql',
        label : '@@form.jdbcOptionsBinder.sql@@',
        description : '@@form.jdbcOptionsBinder.sql.desc@@',
        type : 'codeeditor',
        mode : 'sql',
        required : 'true'
    }],
    buttons : [{
        name : 'testConnection',    
        label : '@@form.jdbcOptionsBinder.testConnection@@',
        ajax_url : '[CONTEXT_PATH]/web/json/app[APP_PATH]/plugin/org.joget.tutorial.JdbcOptionsBinder/service?action=testConnection',
        fields : ['jdbcDriver', 'jdbcUrl', 'jdbcUser', 'jdbcPassword'],
        control_field: 'jdbcDatasource',
        control_value: 'custom',
        control_use_regex: 'false'
    }]
}]

在“属性选项”中,我们添加了一个按钮,用于在使用自定义数据源时测试连接。这个按钮会调用一个JSON API来做测试。所以,我们的插件将需要实现  org.joget.plugin.base.PluginWebSupport 接口,使其成为一个  Web服务插件。让我们按如下方式实现webService方法来测试JDBC连接。

    /**
     * JSON API for test connection button
     * @param request
     * @param response
     * @throws ServletException
     * @throws IOException 
     */
    public void webService(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //Limit the API for admin usage only
        boolean isAdmin = WorkflowUtil.isCurrentUserInRole(WorkflowUserManager.ROLE_ADMIN);
        if (!isAdmin) {
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
            return;
        }
        
        String action = request.getParameter("action");
        if ("testConnection".equals(action)) {
            String message = "";
            Connection conn = null;
            try {
                AppDefinition appDef = AppUtil.getCurrentAppDefinition();
                
                String jdbcDriver = AppUtil.processHashVariable(request.getParameter("jdbcDriver"), null, null, null, appDef);
                String jdbcUrl = AppUtil.processHashVariable(request.getParameter("jdbcUrl"), null, null, null, appDef);
                String jdbcUser = AppUtil.processHashVariable(request.getParameter("jdbcUser"), null, null, null, appDef);
                String jdbcPassword = AppUtil.processHashVariable(SecurityUtil.decrypt(request.getParameter("jdbcPassword")), null, null, null, appDef);
                
                Properties dsProps = new Properties();
                dsProps.put("driverClassName", jdbcDriver);
                dsProps.put("url", jdbcUrl);
                dsProps.put("username", jdbcUser);
                dsProps.put("password", jdbcPassword);
                DataSource ds = BasicDataSourceFactory.createDataSource(dsProps);
                
                conn = ds.getConnection();
                
                message = AppPluginUtil.getMessage("form.jdbcOptionsBinder.connectionOk", getClassName(), MESSAGE_PATH);
            } catch (Exception e) {
                LogUtil.error(getClassName(), e, "Test Connection error");
                message = AppPluginUtil.getMessage("form.jdbcOptionsBinder.connectionFail", getClassName(), MESSAGE_PATH) + "\n"  + e.getMessage();
            } finally {
                try {
                    if (conn != null && !conn.isClosed()) {
                        conn.close();
                    }
                } catch (Exception e) {
                    LogUtil.error(DynamicDataSourceManager.class.getName(), e, "");
                }
            }
            try {
                JSONObject jsonObject = new JSONObject();
                jsonObject.accumulate("message", message);
                jsonObject.write(response.getWriter());
            } catch (Exception e) {
                //ignore
            }
        } else {
            response.setStatus(HttpServletResponse.SC_NO_CONTENT);
        }
    }

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

    public FormRowSet loadAjaxOptions(String[] dependencyValues) {
        FormRowSet rows = new FormRowSet();
        rows.setMultiRow(true);
        
        //add empty option based on setting
        if ("true".equals(getPropertyString("addEmpty"))) {
            FormRow empty = new FormRow();
            empty.setProperty(FormUtil.PROPERTY_LABEL, getPropertyString("emptyLabel"));
            empty.setProperty(FormUtil.PROPERTY_VALUE, "");
            rows.add(empty);
        }
        
        //Check the sql. If require dependency value and dependency value is not exist, return empty result.
        String sql = getPropertyString("sql");
        if ((dependencyValues == null || dependencyValues.length == 0) && sql.contains("?")) {
            return rows;
        }
        
        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        
        try {
            DataSource ds = createDataSource();
            con = ds.getConnection();
            
            //support for multiple dependency values
            if (sql.contains("?") && dependencyValues != null && dependencyValues.length > 1) {
                String mark = "?";
                for (int i = 1; i < dependencyValues.length; i++) {
                    mark += ", ?";
                }
                sql = sql.replace("?", mark);
            }
            
            pstmt = con.prepareStatement(sql);
            
            //set query parameters
            if (sql.contains("?") && dependencyValues != null && dependencyValues.length > 0) {
                for (int i = 0; i < dependencyValues.length; i++) {
                    pstmt.setObject(i + 1, dependencyValues[i]);
                }
            }
            
            rs = pstmt.executeQuery();
            ResultSetMetaData rsmd = rs.getMetaData();
            int columnsNumber = rsmd.getColumnCount();
            
            // Set retrieved result to Form Row Set
            while (rs.next()) {
                FormRow row = new FormRow();
                
                String value = rs.getString(1);
                String label = rs.getString(2);
                
                row.setProperty(FormUtil.PROPERTY_VALUE, (value != null)?value:"");
                row.setProperty(FormUtil.PROPERTY_LABEL, (label != null)?label:"");
                
                if (columnsNumber > 2) {
                    String grouping = rs.getString(3);
                    row.setProperty(FormUtil.PROPERTY_GROUPING, grouping);
                }
                
                rows.add(row);
            }
        } 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;
    }
    
    /**
     * 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_options_binder / src / main”目录下创建目录“resources / messages”。然后,在该文件夹中创建一个“JdbcOptionsBinder.properties”文件。在属性文件中,让我们添加所有的消息键和标签如下。

org.joget.tutorial.JdbcOptionsBinder.pluginLabel=JDBC Binder
org.joget.tutorial.JdbcOptionsBinder.pluginDesc=Used to load field's options using JDBC
form.jdbcOptionsBinder.config=Configure JDBC Binder
form.jdbcOptionsBinder.datasource=Datasource
form.jdbcOptionsBinder.customDatasource=Custom Datasource
form.jdbcOptionsBinder.defaultDatasource=Default Datasource
form.jdbcOptionsBinder.driver=Custom JDBC Driver
form.jdbcOptionsBinder.driver.desc=Eg. com.mysql.jdbc.Driver (MySQL), oracle.jdbc.driver.OracleDriver (Oracle), com.microsoft.sqlserver.jdbc.SQLServerDriver (Microsoft SQL Server)
form.jdbcOptionsBinder.url=Custom JDBC URL
form.jdbcOptionsBinder.username=Custom JDBC Username
form.jdbcOptionsBinder.password=Custom JDBC Password
form.jdbcOptionsBinder.useAjax=Use AJAX for cascade options?
form.jdbcOptionsBinder.addEmpty=Add Empty Option?
form.jdbcOptionsBinder.emptyLabel=Empty Option Label
form.jdbcOptionsBinder.sql=SQL SELECT Query
form.jdbcOptionsBinder.sql.desc=Use question mark (?) in your query to represent dependency values when using AJAX
form.jdbcOptionsBinder.testConnection=Test Connection
form.jdbcOptionsBinder.connectionOk=Database connected
form.jdbcOptionsBinder.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(JdbcOptionsBinder.class.getName(), new JdbcOptionsBinder(), null));
    } 

f. 构建并测试

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

然后,让我们上传插件jar到  管理插件。上传jar文件后,再次检查插件是否正确上传并激活。

然后,让我们 在表单中创建一个  AJAX级联下拉列表来测试它。我们来创建我们的测试表单,如下所示。

然后,配置我们的选择框和JDBC绑定。

在查询中,我们将使用以下查询来获取基于组ID的用户列表。

select distinct username, firstName, groupId from dir_user u 
join dir_user_group g on u.username=g.userId 
where groupId in (?) group by username;

将依赖关系配置为“group”。然后,测试结果。

用户选择框选项根据组选择框的选定值进行更改。

现在,让我们将查询更改为以下内容,以便在不使用AJAX的情况下测试级联下拉列表。

select distinct username, firstName, groupId from dir_user u 
join dir_user_group g on u.username=g.userId 
group by username;

请记住取消勾选“ 使用AJAX进行级联选项? ”选项,以使其不使用AJAX。

是的,它也可以工作。然后,我们可以测试自定义配置和测试连接按钮。

8. 再进一步,分享或出售

您可以从jdbc_options_binder_src.zip下载源代码  。

要下载现成的插件jar,请在http://marketplace.joget.org/上找到它  。

 


 

 

  • No labels