在本教程中,我们将遵循 guideline for developing a plugin 来开发一个下载PDF 数据列表插件.有关更多详细信息步骤,请参阅第一个教程 如何开发一个Bean Shell Hash Variable插件.

1.什么问题?

我们需要能够从datalist下载PDF格式的表格数据。

2.如何解决问题?

我们将开发一个  Datalist Action插件  来显示一个按钮来生成一个表单PDF文件。 

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

要开发一个PDF下载Datalist Action插件,我们将考虑提供以下内容作为输入。

  1. 表单ID:将用于生成PDF文件的表单。
  2. 记录标识列:使用数据列行的标识或列值来加载记录。
  3. 格式化选项:用于格式化和自定义PDF输出的选项。 

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

当“PDF Download Datalist Action”用作数据列行操作或列操作时,普通用户将在数据列表的每一行中看到链接以下载PDF文件。一旦链接被点击,一个PDF文件将被提示下载该特定的行。

当插件用于多个datalist行(整个列表操作)时,包含所有生成的每个选定行的PDF文件的zip文件将被点击按钮时提示下载。

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

要开发PDF下载Datalist Action插件,我们可以重用FormPdfUtil中的方法   来生成PDF格式的表单。我们也可以参考 Datalist Form Data Delete Action插件的  源代码。除此之外,我们可以参考  导出表单电子邮件工具  ,了解我们可以在插件中提供哪种插件属性选项,因为导出表单电子邮件工具也使用FormPdfUtil中的方法。

6.准备你的开发环境

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

以下教程是用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 download_pdf_datalist_action 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: download_pdf_datalist_action
version: 5.0.0
package: org.joget.tutorial
Y: : y

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

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

7. 开始编码吧!

a. 继承插件抽象类

在“org.joget.tutorial”包下创建一个“DownloadPdfDatalistAction”类。然后,使用org.joget.apps.datalist.model.DataListActionDefault抽象类来扩展  该类。请参阅  Datalist Action插件

b. 实现所有的抽象方法

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

package org.joget.tutorial;
 
import org.joget.apps.app.service.AppPluginUtil;
import org.joget.apps.datalist.model.DataListActionDefault;
 
public class DownloadPdfDatalistAction extends DataListActionDefault {
    
    private final static String MESSAGE_PATH = "messages/DownloadPdfDatalistAction";
 
    public String getName() {
        return "Download PDF Datalist Action";
    }
    public String getVersion() {
        return "5.0.0";
    }
    
    public String getClassName() {
        return getClass().getName();
    }
    
    public String getLabel() {
        //support i18n
        return AppPluginUtil.getMessage("org.joget.tutorial.DownloadPdfDatalistAction.pluginLabel", getClassName(), MESSAGE_PATH);
    }
    
    public String getDescription() {
        //support i18n
        return AppPluginUtil.getMessage("org.joget.tutorial.DownloadPdfDatalistAction.pluginDesc", getClassName(), MESSAGE_PATH);
    }
 
    public String getPropertyOptions() {
        return AppUtil.readPluginResource(getClassName(), "/properties/downloadPdfDatalistAction.json", null, true, MESSAGE_PATH);
    }
 
    public String getLinkLabel() {
        return getPropertyString("label"); //get label from configured properties options
    }
 
    public String getHref() {
        return getPropertyString("href"); //Let system to handle to post to the same page
    }
 
    public String getTarget() {
        return "post";
    }
 
    public String getHrefParam() {
        return getPropertyString("hrefParam");  //Let system to set the parameter to the checkbox name
    }
 
    public String getHrefColumn() {
        String recordIdColumn = getPropertyString("recordIdColumn"); //get column id from configured properties options
        if ("id".equalsIgnoreCase(recordIdColumn) || recordIdColumn.isEmpty()) {
            return getPropertyString("hrefColumn"); //Let system to set the primary key column of the binder
        } else {
            return recordIdColumn;
        }
    }
 
    public String getConfirmation() {
        return getPropertyString("confirmation"); //get confirmation from configured properties options
    }
 
    public DataListActionResult executeAction(DataList dataList, String[] rowKeys) {
        throw new UnsupportedOperationException("Not supported yet.");
    }
}

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

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

[{
    title : '@@datalist.downloadPdf.config@@',
    properties : [{
        name : 'label',
        label : '@@datalist.downloadPdf.label@@',
        type : 'textfield',
        value : '@@datalist.downloadPdf.download@@'
    },
    {
        name : 'formDefId',
        label : '@@datalist.downloadPdf.form@@',
        type : 'selectbox',
        options_ajax : '[CONTEXT_PATH]/web/json/console/app[APP_PATH]/forms/options',
        required : 'True'
    },
    {
        name : 'recordIdColumn',
        label : '@@datalist.downloadPdf.recordIdColumn@@',
        description : '@@datalist.downloadPdf.recordIdColumn.desc@@',
        type : 'textfield'
    },
    {
        name : 'confirmation',
        label : '@@datalist.downloadPdf.confirmationMessage@@',
        type : 'textfield'
    }]
},
{
    title : '@@datalist.downloadPdf.advanced@@',
    properties : [{
        name : 'formatting',
        label : '@@datalist.downloadPdf.formatting@@',
        type : 'codeeditor',
        mode : 'css'
    },
    {
        name : 'headerHtml',
        label : '@@datalist.downloadPdf.headerHtml@@',
        type : 'codeeditor',
        mode : 'html'
    },
    {
        name : 'repeatHeader',
        label : '@@datalist.downloadPdf.repeatHeader@@',
        type : 'checkbox',
        options : [{
            value : 'true',
            label : ''
        }]
    },
    {
        name : 'footerHtml',
        label : '@@datalist.downloadPdf.footerHtml@@',
        type : 'codeeditor',
        mode : 'html'
    },
    {
        name : 'repeatFooter',
        label : '@@datalist.downloadPdf.repeatFooter@@',
        type : 'checkbox',
        options : [{
            value : 'true',
            label : ''
        }]
    },
    {
        name : 'hideEmptyValueField',
        label : '@@datalist.downloadPdf.hideEmptyValueField@@',
        type : 'checkbox',
        options : [{
            value : 'true',
            label : ''
        }]
    },
    {
        name : 'showNotSelectedOptions',
        label : '@@datalist.downloadPdf.showNotSelectedOptions@@',
        type : 'checkbox',
        options : [{
            value : 'true',
            label : ''
        }]
    }]
}]

完成属性选项以收集输入后,我们可以对插件的主要方法(即executeAction方法)进行操作。

    public DataListActionResult executeAction(DataList dataList, String[] rowKeys) {
        // only allow POST
        HttpServletRequest request = WorkflowUtil.getHttpServletRequest();
        if (request != null && !"POST".equalsIgnoreCase(request.getMethod())) {
            return null;
        }
        
        // check for submited rows
        if (rowKeys != null && rowKeys.length > 0) {
            try {
                //get the HTTP Response
                HttpServletResponse response = WorkflowUtil.getHttpServletResponse();
                if (rowKeys.length == 1) {
                    //generate a pdf for download
                    singlePdf(request, response, rowKeys[0]);
                } else {
                    //generate a zip of all pdfs
                    multiplePdfs(request, response, rowKeys);
                }
            } catch (Exception e) {
                LogUtil.error(getClassName(), e, "Fail to generate PDF for " + ArrayUtils.toString(rowKeys));
            }
        }
        
        //return null to do nothing
        return null;
    }
    
    /**
     * Handles for single pdf file
     * @param request
     * @param response
     * @param rowKey
     * @throws IOException 
     * @throws javax.servlet.ServletException 
     */
    protected void singlePdf(HttpServletRequest request, HttpServletResponse response, String rowKey) throws IOException, ServletException {
        byte[] pdf = getPdf(rowKey);
        writeResponse(request, response, pdf, rowKey+".pdf", "application/pdf");
    }
    
    /**
     * Handles for multiple files download. Put all pdfs in zip.
     * @param request
     * @param response
     * @param rowKeys 
     * @throws java.io.IOException 
     * @throws javax.servlet.ServletException 
     */
    protected void multiplePdfs(HttpServletRequest request, HttpServletResponse response, String[] rowKeys) throws IOException, ServletException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ZipOutputStream zip = new ZipOutputStream(baos);
        
        try {
            //create pdf and put in zip
            for (String id : rowKeys) {
                byte[] pdf = getPdf(id);
                zip.putNextEntry(new ZipEntry(id+".pdf"));
                zip.write(pdf);
                zip.closeEntry();
            }
 
            zip.finish();
            writeResponse(request, response, baos.toByteArray(), getLinkLabel() +".zip", "application/zip");
        } finally {
            baos.close();
            zip.flush();
        }
    }
    
    /**
     * Generate PDF using FormPdfUtil
     * @param id
     * @return 
     */
    protected byte[] getPdf(String id) {
        AppDefinition appDef = AppUtil.getCurrentAppDefinition();
        String formDefId = getPropertyString("formDefId");
 
        Boolean hideEmptyValueField = null;
        if (getPropertyString("hideEmptyValueField").equals("true")) {
            hideEmptyValueField = true;
        }
        Boolean showNotSelectedOptions = null;
        if (getPropertyString("showNotSelectedOptions").equals("true")) {
            showNotSelectedOptions = true;
        }
        Boolean repeatHeader = null;
        if ("true".equals(getPropertyString("repeatHeader"))) {
            repeatHeader = true;
        }
        Boolean repeatFooter = null;
        if ("true".equals(getPropertyString("repeatFooter"))) {    
            repeatFooter = true;
        }
        String css = null;
        if (!getPropertyString("formatting").isEmpty()) {
            css = getPropertyString("formatting");
        }
        String header = null; 
        if (!getPropertyString("headerHtml").isEmpty()) {
            header = getPropertyString("headerHtml");
            header = AppUtil.processHashVariable(header, null, null, null);
        }
        String footer = null; 
        if (!getPropertyString("footerHtml").isEmpty()) {
            footer = getPropertyString("footerHtml");
            footer = AppUtil.processHashVariable(footer, null, null, null);
        }
        
        return FormPdfUtil.createPdf(formDefId, id, appDef, null, hideEmptyValueField, header, footer, css, showNotSelectedOptions, repeatHeader, repeatFooter);
    }
    
    /**
     * Write to response for download
     * @param response
     * @param bytes
     * @param filename
     * @param contentType
     * @throws IOException 
     */
    protected void writeResponse(HttpServletRequest request, HttpServletResponse response, byte[] bytes, String filename, String contentType) throws IOException, ServletException {
        OutputStream out = response.getOutputStream();
        try {
            String name = URLEncoder.encode(filename, "UTF8").replaceAll("\\+", "%20");
            response.setHeader("Content-Disposition", "attachment; filename="+name+"; filename*=UTF-8''" + name);
            response.setContentType(contentType+"; charset=UTF-8");
            
            if (bytes.length > 0) {
                response.setContentLength(bytes.length);
                out.write(bytes);
            }
        } finally {
            out.flush();
            out.close();
            
            //simply foward to a 
            request.getRequestDispatcher(filename).forward(request, response);
        }
    }

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

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

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

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

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

在“download_pdf_datalist_action / src / main”目录下创建一个目录“resources / messages”。然后,在文件夹中创建一个“DownloadPdfDatalistAction.properties”文件。在属性文件中,添加所有消息密钥及其标签,如下所示。

org.joget.tutorial.DownloadPdfDatalistAction.pluginLabel=Download PDF
org.joget.tutorial.DownloadPdfDatalistAction.pluginDesc=Support to download form PDF from datalist
datalist.downloadPdf.download=Download
datalist.downloadPdf.config=Configure Download PDF Action
datalist.downloadPdf.label=Label
datalist.downloadPdf.form=Form
datalist.downloadPdf.recordIdColumn=Record Id Column
datalist.downloadPdf.recordIdColumn.desc=Default to the primary key of the configured binder
datalist.downloadPdf.confirmationMessage=Confirmation Message
datalist.downloadPdf.hideEmptyValueField=Hide field that without value
datalist.downloadPdf.showNotSelectedOptions=Show unselected options for multi options field
datalist.downloadPdf.advanced=Advanced
datalist.downloadPdf.formatting=Formatting (CSS)
datalist.downloadPdf.headerHtml=Header (HTML)
datalist.downloadPdf.repeatHeader=Repeat header on every page?
datalist.downloadPdf.footerHtml=Footer (HTML)
datalist.downloadPdf.repeatFooter=Repeat footer on every page?

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

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

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

f. 构建并测试

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

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

那么,让我们来试一下datalist。您可以在Datalist Builder的 “操作”下看到我们的新插件。

一旦我们将“下载PDF”操作拖放到数据生成器画布中,我们就可以编辑该操作。以下配置页面将根据我们的属性选项定义显示。

 

我们添加“下载PDF”操作作为行操作,也是整个列表操作进行测试。我们可以在下面的userview截图中看到正确显示的“下载”按钮。

当点击时,下载pdf。

当整个列表动作被点击时,一个zip文件被下载。

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

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

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