ในบทเรียนนี้เราจะทำตาม guideline of developing a plugin เพื่อพัฒนาปลั๊กอิน JDBC Form Store Binder ของเรา โปรดอ้างอิงถึงบทช่วยสอนแรกสุด วิธีการพัฒนา Bean Shell Hash Variable และปลั๊กอินที่เกี่ยวข้องกับ JDBC ต่อไปนี้สำหรับขั้นตอนรายละเอียดเพิ่มเติม

1. ปัญหาคืออะไร?

เพื่อจุดประสงค์ในการรวมเราต้องการจัดเก็บข้อมูลฟอร์มของเราลงในตารางฐานข้อมูลอื่นแทนตารางข้อมูล Joget.

2. ความคิดของคุณในการแก้ปัญหาคืออะไร?

Joget Workflow จัดทำประเภทปลั๊กอินที่เรียกว่า Form Store Binder Pluginเราจะพัฒนาเพื่อรองรับการเชื่อมต่อ JDBC และ query ที่กำหนดเองเพื่อเก็บข้อมูลแบบฟอร์ม

3. อินพุตที่จำเป็นสำหรับปลั๊กอินของคุณ?

เพื่อการพัฒนาปลั๊กอิน JDBC Store binder, เราจะต้องมีการตั้งค่าการเชื่อมต่อ JDBC และ query แบบกำหนดเองเพื่อเก็บข้อมูลฟอร์มตามข้อมูลฟอร์มที่รวบรวม

  1. Datasource: การใช้แหล่งข้อมูลที่กำหนดเองหรือแหล่งข้อมูลเริ่มต้นของ Joget
  2. Custom JDBC Driver: ไดรเวอร์ JDBC สำหรับแหล่งข้อมูลที่กำหนดเอง
  3. Custom JDBC URL: URL การเชื่อมต่อ JDBC สำหรับแหล่งข้อมูลที่กำหนดเอง
  4. Custom JDBC Username: ชื่อผู้ใช้สำหรับแหล่งข้อมูลที่กำหนดเอง
  5. Custom JDBC Password: รหัสผ่านสำหรับแหล่งข้อมูลที่กำหนดเอง
  6. SQL Check Exist Query: The query เพื่อตรวจสอบว่าแบบสอบถามแทรกหรือปรับปรุงควรจะดำเนินการ
  7. SQL Insert Query: The query เพื่อแทรกข้อมูลแบบฟอร์ม
  8. SQL Update Query: The query เพื่อแทรกข้อมูลแบบฟอร์ม
  9. SQL Delete Query: The query เพื่อลบข้อมูลในแบบฟอร์มที่ถูกลบเมื่อใช้เป็น multirow binder

เราจะต้องสนับสนุน syntax ในการเอาข้อมูลแบบฟอร์มไปยัง query "{foreignKey}" สามารถใช้สำหรับการจัดเก็บหลายแถว

เราจะต้องสนับสนุน syntax ในการเอาข้อมูลแบบฟอร์มไปยัง UUID value. ในกรณีนี้เราจะใช้ "{uuid}".

ตัวอย่าง: INSERT INTO app_fd_test VALUES ({id}, {name}, {email}, {phone}, {foreignKey});

4. ผลลัพธ์และผลลัพธ์ที่คาดหวังจากปลั๊กอินของคุณคืออะไร?

ข้อมูลที่ส่งทั้งหมดจะเก็บตามคำสั่ง check / insert / update

5. มีทรัพยากร / API ใด ๆ ที่สามารถนำมาใช้ซ้ำได้?

เราสามารถอ้างถึงการดำเนินการของอื่น ๆ ที่มีอยู่ใน Form Store Binder plugins ได้ แหล่งข้อมูลเริ่มต้นของ Joget สามารถเรียกดูได้ที่ AppUtil.getApplicationContext().getBean("setupDataSource").

6. เตรียม environment ของคุณเพื่อการพัฒนา

เราจำเป็นต้องเตรียมซอร์สโค้ด Joget Workflow ของเราให้พร้อมและสร้างโดยทำตาม this guideline

บทเรียนนี้จัดทำขึ้นด้วย Macbook Pro และ Joget Source Code version 5.0.0. โปรดอ้างอิงถึง แนวทางสำหรับการพัฒนาปลั๊กอิน สำหรับคำสั่งแพลตฟอร์มอื่น ๆ

ให้กล่าวว่าไดเรกทอรีโฟลเดอร์ของเราดังต่อไปนี้

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

ไดเรกทอรี "ปลั๊กอิน" คือโฟลเดอร์ที่เราจะสร้างและจัดเก็บปลั๊กอินทั้งหมดของเราและไดเรกทอรี "jw-community" เป็นที่เก็บซอร์สโค้ด Joget Workflow

เรียกใช้คำสั่งต่อไปนี้เพื่อสร้างโครงการ 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

จากนั้น the shell script จะขอให้เราใส่ปลั๊กอินสำหรับรุ่นของคุณและขอให้เรายืนยันก่อนที่จะสร้างโครงการ 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" ที่ปรากฏในเครื่องของเราและโฟลเดอร์ "jdbc_store_binder" ที่สร้างในโฟลเดอร์ "ปลั๊กอิน"

เปิดโครงการ maven ด้วย IDE ที่คุณโปรดปราน เราแนะนำให้ใช้ NetBeans.  

7. เริ่มโค้ด!

a. การขยาย abstract class ของประเภทปลั๊กอิน

สร้างคลาส "JdbcStoreBinder" ภายใต้ "org.joget.tutorial" package. จากนั้นขยายคลาสด้วย org.joget.apps.form.model.FormBinder abstract class.

เพื่อให้มันทำงานเป็น Form Store Binder, เราจะต้องใช้อินเตอร์เฟสของ  org.joget.apps.form.model.FormStoreBinder. จากนั้นเราจำเป็นต้องใช้อินเตอร์เฟส org.joget.apps.form.model.FormStoreElementBinder เพื่อให้ปลั๊กอินนี้แสดงเป็นตัวเลือกใน Select Box และเลือกใช้อินเตอร์เฟสของ org.joget.apps.form.model.FormStoreMultiRowElementBinder เพื่อแสดงรายการภายใต้ Select Box ของ grid element.

โปรดอ้างอิงถึง Form Store Binder Plugin. 

b. การดำเนินการของ abstract methods ทั้งหมด

เช่นเคย เราต้องใช้ abstract methods ทั้งหมด เราจะใช้ AppPluginUtil.getMessage method เพื่อสนับสนุน i18n และใช้ตัวแปรคงที่ MESSAGE_PATH สำหรับ message resource bundle directory.

 

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.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 method, เราได้เตรียม ตัวเลือกคุณสมบัติปลั๊กอิน ไว้ที่ไฟล์ "/properties/jdbcStoreBinder.json". ให้เราสร้าง directory "resources/properties" ภายใต้ "jdbc_store_binder/src/main" directory. หลังจากสร้าง directory, ให้สร้างไฟล์ชื่อ "jdbcStoreBinder.json" ในโฟลเดอร์ "properties"

ในไฟล์ตัวเลือกคุณสมบัติเราจะต้องเลือกตัวเลือกดังนี้ โปรดทราบว่าเราสามารถใช้ "@@message.key@@" syntax เพื่อสนับสนุน 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 Options Binder บน Web Service Plugin.

เมื่อเราทำตัวเลือกคุณสมบัติเพื่อรวบรวมอินพุตและบริการเว็บเพื่อทดสอบการเชื่อมต่อเราสามารถทำงานกับวิธีการหลักของปลั๊กอินซึ่งเป็นวิธีการจัดเก็บ

    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. จัดการ dependency libraries ของปลั๊กอินของคุณ

ปลั๊กอินของเราใช้ dbcp, javax.servlet.http.HttpServletRequest และ javax.servlet.http.HttpServletResponse class, ดังนั้นเราต้องเพิ่ม jsp-api และ commons-dbcp library ไปที่ไฟล์ 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. เตรียมปลั๊กอิน internationalization (i18n) ของคุณให้พร้อม

เรากำลังใช้ i18n message key ใน getLabel และ getDescription method. และเรายังใช้ i18n message key ในตัวเลือกคุณสมบัติของเราด้วย. ดังนั้นเราต้องสร้างไฟล์คุณสมบัติ message resource bundle เพื่อปลั๊กอินของเรา

สร้าง directory "resources/messages" ภายใต้ "jdbc_store_binder/src/main" directory. จากนั้นสร้างไฟล์ "JdbcStoreBinder.properties" ในโฟลเดอร์. ในไฟลืคุณสมบัติให้เราเพิ่ม message keys และ label ดังนี้

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 Framework

เราจะต้องลงทะเบียนคลาสปลั๊กอินของเราในคลาส Activator (Auto generated in the same class package) เพื่อบอก Felix Framework ว่านี่เป็นปลั๊กอิน

    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-5.0.0.jar" ภายใต้ไดเรกทอรี "jdbc_store_binder / target"

จากนั้นให้อัปโหลด jar ปลั๊กอินไปที่ Manage Plugins หลังจากอัปโหลดไฟล์ jar ตรวจสอบอีกครั้งว่าปลั๊กอินถูกอัปโหลดและเปิดใช้งานอย่างถูกต้อง

ให้สร้างแบบฟอร์มเพื่อสร้างและอัปเดตผู้ใช้เป็นตาราง dir_user

จากนั้นกำหนดค่า binder เก็บของฟอร์มด้วย query ต่อไปนี้

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

note: {uuid} สามารถใช้สร้าง unique id

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

 

Delete Query
delete from TABLE_NAME where id = {id}

ตอนนี้ให้ทดสอบเพื่อเพิ่มผู้ใช้

ตรวจสอบผู้ใช้ที่ถูกสร้างขึ้นในตาราง dir_user

ให้อัปเดตระเบียนเดียวกันโดยส่งรหัสในพารามิเตอร์ URL

ตรวจสอบการปรับปรุงผู้ใช้

มันได้ผล! โปรดอย่าลืมทดสอบคุณสมบัติอื่น ๆ ของปลั๊กอินด้วย (big grin)

8. ขั้นต่อไป แชร์หรือขาย

คุณสามารถดาวน์โหลด source code จาก jdbc_store_binder_src.zip

หากต้องการดาวน์โหลด jar ปลั๊กอินที่พร้อมใช้งานโปรดค้นหา http://marketplace.joget.org/.

 

  • No labels