【仅供内部供应商使用,不提供对外解答和培训】
背景:以往我们对上传和下载类的插件实现没有任何要求,只要求功能实现即可,导致了大家各种奇思妙想的迸发,设计和实现方案迥异。最终导致移动端完全无法适配(每个插件单独适配成本高、耦合重、维护难)
在跟移动端组协商后,我们统一以下的接口标准
移动端那边的适配工作预计在2021年1月底完成。以后所有遵循此标准开发的报表上传下载插件均可在移动端使用
1、报表插件中所有文件的写功能,必须使用填报接口实现
2、报表插件中所有文件下载功能,必须使用超链接接口实现
3、所有填报功能实现必须完全不依赖前端代码开发(也就是上传功能不得使用前端实现,全部依赖产品自带的文件控件)。
4、所有下载插件的前端必须统一以FR.downloadHyperlink( taskId ) 为入口实现下载触发
5、所有文件下载的服务必须统一为两个web接口:
FR.servletURL+"?op=fr_attach&cmd=ah_info&id="+taskId
功能:获取要下载的文件的基础信息(包括判断文件的有效性)
响应:{ success:true, id:"$TASKID", filename: "下载的文件名(重命名后的)", size:"文件的大小byte" } 或者 { success:false, error_msg:"错误的详细内容(文件不存在/连接失败等等的)" }
FR.servletURL+"?op=fr_attach&cmd=ah_download&id="+taskId
功能:产品内部已有的附件下载接口(我们下载的时候需要借助产品的附件这个入口实现)
响应:文件流
首先我们要依赖于一个插件(标准中有两个请求,但是产品里面第一个获取文件信息的接口并没有实现,所以我们单独做了一个补丁插件把这接口和JS的部分都封装好了)
View file | ||||
---|---|---|---|---|
|
安装这个插件除了获得获取文件信息接口的能力外,插件还提供了一些产品内部计算逻辑和简化逻辑的封装,我们开发自己的插件时plugin.xml不要忘记添加依赖哟
Code Block | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|
| ||||||||||
<dependence> <item type="plugin" key="com.tptj.extra.hg.attachment.service"/> </dependence> |
注:除了plugin.xml要添加依赖之外,开发的时候也不要忘记依赖插件内的3个JAR包哟!
接下来我们开发一个基础的从磁盘上传或者下载文件的插件作为demo示例
首先我们要定义我们描述一个文件的对象,我们简单分为,路径/名称/重命名 3个属性(大家实际开发的时候,具体有多少种属性,属性叫啥自己根据需要定义即可),因为这些属性我们都支持公式,也允许单元格扩展填报动态计算,那么我们属性值就都定义成Object
Code Block | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
| ||||||||||||
package com.tptj.bridge.hg.file.load.demo; import com.fr.io.context.info.GetConfig; import com.fr.stable.FCloneable; import com.tptj.tools.hg.file.operator.dynamics.Column; import com.tptj.tools.hg.file.operator.utils.BaseUtils; import com.tptj.tools.hg.xml.fun.Config; /** * @author 秃破天际 * @version 10.0 * Created by 秃破天际 on 2021-01-07 **/ public class FileDefine implements FCloneable { public static final String KEY_PATH = "path"; public static final String KEY_FILE = "file"; public static final String KEY_RENAME = "rename"; @Config(KEY_PATH) @Column(title = "路径",idx = 0) private Object path; @Config(KEY_FILE) @Column(title = "文件",idx = 1) private Object file; @Config(KEY_RENAME) @Column(title = "重命名",idx = 2) private Object rename; @GetConfig(KEY_PATH) public Object getPath() { return path; } public void setPath(Object path) { this.path = path; } @GetConfig(KEY_FILE) public Object getFile() { return file; } public void setFile(Object file) { this.file = file; } @GetConfig(KEY_RENAME) public Object getRename() { return rename; } public void setRename(Object rename) { this.rename = rename; } @Override public Object clone() throws CloneNotSupportedException { FileDefine obj = (FileDefine)super.clone(); obj.file = BaseUtils.clone( file ); obj.path = BaseUtils.clone( path ); obj.rename = BaseUtils.clone( rename ); return obj; } } |
因为涉及动态计算(单元格扩展/填报编辑等等的),所以我们的对象一定要实现FCloneable这个接口(扩展的时候会涉及到对象的拷贝)
成员带@Config注解,可以在读写XML和动态计算的时候自动被识别和转换
成员带@Column注解 可以在构建配置界面时自动被识别到并生成配置界面(单对象是列表,多对象是表格,这里先知道一个概念即可,具体代表什么,后面看效果一眼就清楚了)
方法有GetConfig注解的,如果是动态计算时,在最终获取结果的时候不用自己单独再计算
BaseUtils.clone 是个基础的对象克隆方法,把实现克隆方法的成员再次克隆而已,没啥特别的。不用关注。
接下来我们先实现下载功能,我们的辅助JAR包里面提供了一个FileDownloadHyperlink类,我们下载的实例直接继承他
Code Block | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
| ||||||||||||
package com.tptj.bridge.hg.file.load.demo; import com.fr.log.FineLoggerFactory; import com.fr.plugin.transform.ExecuteFunctionRecord; import com.fr.plugin.transform.FunctionRecorder; import com.fr.stable.StableUtils; import com.fr.stable.StringUtils; import com.tptj.tools.hg.file.operator.bridge.FileDownloadHyperlink; import com.tptj.tools.hg.file.operator.utils.DynamicsConfigUtils; import com.tptj.tools.hg.xml.fun.Config; import java.io.FileInputStream; import java.io.InputStream; /** * @author 秃破天际 * @version 10.0 * Created by 秃破天际 on 2021-01-04 **/ @FunctionRecorder public class DownloadHyperlink extends FileDownloadHyperlink { @Config("file") private FileDefine file; public FileDefine getFile() { return file; } public void setFile(FileDefine file) { this.file = file; } @Override public String getOutputFilename() { String val = DynamicsConfigUtils.getConfig( file.getRename() ); if( StringUtils.isEmpty(val) ){ val = DynamicsConfigUtils.getConfig( file.getFile() ); } return val; } @Override @ExecuteFunctionRecord public InputStream load() { try{ return new FileInputStream( StableUtils.pathJoin( (String) DynamicsConfigUtils.getConfig( file.getPath() ), (String)DynamicsConfigUtils.getConfig( file.getFile() ) ) ); }catch(Exception e){ } return null; } @Override public Object clone() throws CloneNotSupportedException { DownloadHyperlink item = (DownloadHyperlink)super.clone(); try{ item.file = ( FileDefine )file.clone(); }catch(Exception e){ FineLoggerFactory.getLogger().error(e,e.getMessage()); } return item; } } |
这里我们声明一个我们定义的文件描述对象FileDefine file,并加了@Config注解,便于XML读写(没有这个注解我们就要自己实现xml的读写和接口了)
同样,因为涉及动态计算所以需要我们实现克隆的方法
然后这个接口还需要我们实现两个方法
获取输出的文件名:getOutputFilename
获取输出的文件流:load
这里额外我们用到了一个API, DynamicsConfigUtils.getConfig,这个其实是还没有设计得很好的,所以需要单独用一个方法在下载的时候,从配置中取出当前的实际值。(填报就不需要这么麻烦了,这个后面我再看看能不能改进吧)
我们实现了下载的对象之后,接下来我们就要实现配置界面了
Code Block | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
| ||||||||||||
package com.tptj.bridge.hg.file.load.demo; import com.fr.design.beans.BasicBeanPane; import com.fr.design.layout.FRGUIPaneFactory; import com.tptj.tools.hg.file.operator.design.DynamicsListPane; import com.tptj.tools.hg.file.operator.utils.DynamicsPaneUtils; import java.awt.*; /** * @author 秃破天际 * @version 10.0 * Created by 秃破天际 on 2021-01-04 **/ public class DownloadHyperlinkPane extends BasicBeanPane<DownloadHyperlink> { DynamicsListPane<FileDefine> editor ; public DownloadHyperlinkPane(){ setLayout(FRGUIPaneFactory.createM_BorderLayout()); editor = DynamicsPaneUtils.createListPane(FileDefine.class); add( editor, BorderLayout.CENTER ); } @Override public void populateBean( DownloadHyperlink link ) { if( null == link ){ return; } editor.populateBean( link.getFile() ); } @Override public DownloadHyperlink updateBean() { DownloadHyperlink rt = new DownloadHyperlink(); rt.setFile( editor.updateBean() ); return rt; } @Override protected String title4PopupWindow() { return "下载demo"; } } |
这个就是一般的FR设计器插件中常用的beanpane的实现,这里我们单独提供了一个API。 DynamicsPaneUtils.createListPane
它的功能就是,把我们带@Column和@Config注解且实现了FCloneable的对象,转换成支持公式编辑的属性配置界面(后面可以看实际效果)
我们这里除了文件描述以外没有增加任何其他配置,大家开发的时候根据自己的实际需要定义相关配置即可。
最后我们注册到插件里面生效即可
Code Block | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
| ||||||||||||
package com.tptj.bridge.hg.file.load.demo; import com.fr.design.fun.impl.AbstractHyperlinkProvider; import com.fr.design.gui.controlpane.NameObjectCreator; import com.fr.design.gui.controlpane.NameableCreator; import com.fr.general.ComparatorUtils; /** * @author 秃破天际 * @version 10.0 * Created by 秃破天际 on 2021-01-04 **/ public class DownloadHyperlinkBridge extends AbstractHyperlinkProvider { //只需要改这里就可以了 private NameableCreator nameableCreator = new NameObjectCreator("下载demo", DownloadHyperlink.class, DownloadHyperlinkPane.class); @Override public int hashCode() { return nameableCreator.menuName().hashCode(); } @Override public boolean equals(Object obj) { return (obj != null && obj instanceof DownloadHyperlinkBridge) && ComparatorUtils.equals(((DownloadHyperlinkBridge) obj).nameableCreator, nameableCreator); } @Override public NameableCreator createHyperlinkCreator() { return nameableCreator; } } |
Code Block | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|
| ||||||||||
<extra-designer> <HyperlinkProvider class="com.tptj.bridge.hg.file.load.demo.DownloadHyperlinkBridge"/> </extra-designer> |
运行效果如下:
接下来我们再来实现对应的上传功能,下载我们每次只能添加一个文件任务(没有做文件夹压缩,感兴趣的同学可以自己在把压缩实现一下即可),那么上传我们希望一次可以多个任务。
首先我们定义上传的操作类,这里我们也封装好了一个接口FileUploadSubmitTask大家直接继承即可
Code Block | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
| ||||||||||||
package com.tptj.bridge.hg.file.load.demo; import com.fr.cache.Attachment; import com.fr.general.GeneralUtils; import com.fr.io.repository.base.fs.FileSystemRepository; import com.fr.log.FineLoggerFactory; import com.fr.plugin.transform.ExecuteFunctionRecord; import com.fr.plugin.transform.FunctionRecorder; import com.fr.stable.Primitive; import com.fr.stable.StableUtils; import com.fr.stable.StringUtils; import com.tptj.tools.hg.file.operator.bridge.FileUploadSubmitTask; import com.tptj.tools.hg.file.operator.dynamics.UploadRowData; /** * @author 秃破天际 * @version 10.0 * Created by 秃破天际 on 2021-01-04 **/ @FunctionRecorder public class FileUploadSubmit extends FileUploadSubmitTask<FileDefine> { public final static String Job_Type = "upload_demo"; @Override @ExecuteFunctionRecord protected void submit( UploadRowData<FileDefine> file ) { file.isDelete();//这条数据是否属于被删除的数据(删除行列) file.isModify();//这条数据是否是(单元格)编辑过的数据 FileDefine conf = file.getRowData(FileDefine.class); try{ Object val = conf.getFile(); if( !(val instanceof Attachment) ){ return; } Attachment attachment = (Attachment) val; val = conf.getPath(); String path = StringUtils.EMPTY; if( !(val instanceof Primitive) ){ path = GeneralUtils.objectToString( conf.getPath() ); } val = conf.getRename(); String rename = StringUtils.EMPTY; if( !(val instanceof Primitive) ){ rename = GeneralUtils.objectToString( conf.getRename() ); } String full_path =StableUtils.pathJoin( path , StringUtils.isNotEmpty( rename )? rename: attachment.getFilename() ); FileSystemRepository.getSingleton().write( full_path, attachment.getInputStream() ); }catch(Exception e){ FineLoggerFactory.getLogger().error(e,e.getMessage()); } } @Override public String getJobType() { return Job_Type; } } |
接口实现我们指明了使用FileDefine作为我们的表格的每一行对象。然后我们需要实现一个submit这个实际提交的操作
其中 UploadRowData<FileDefine> file 这个参数向我们指明了,这个文件是否是编辑过的单元格产生的以及是否是删除任务。
同时通过 FileDefine conf = file.getRowData(FileDefine.class); 我们拿到了动态计算后包含最终结果的文件定义,这个时候我们再获取对应的配置是就不需要像下载那样单独用一个方法去读了,直接get即可(通过我们get方法上的@GetConfig注解实现的,所以我们前面才需要单独加这个注解)
然后我们类似下载需要定义一个界面
Code Block | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
| ||||||||||||
package com.tptj.bridge.hg.file.load.demo; import com.fr.design.beans.BasicBeanPane; import com.fr.design.layout.FRGUIPaneFactory; import com.tptj.tools.hg.file.operator.design.DynamicsTablePane; import com.tptj.tools.hg.file.operator.utils.DynamicsPaneUtils; import java.awt.*; import java.util.List; /** * @author 秃破天际 * @version 10.0 * Created by 秃破天际 on 2021-01-04 **/ public class FileUploadSubmitPane extends BasicBeanPane<FileUploadSubmit> { private DynamicsTablePane<FileDefine> editor; public FileUploadSubmitPane() { setLayout(FRGUIPaneFactory.createM_BorderLayout()); editor = DynamicsPaneUtils.createTablePane(FileDefine.class); add( editor, BorderLayout.CENTER ); } @Override public void populateBean( FileUploadSubmit data ) { if( null == data ){ return; } List<FileDefine> files = data.getFiles(); if( null == files ){ return; } editor.populate( files.toArray( new FileDefine[files.size()] ) ); } @Override public FileUploadSubmit updateBean() { FileUploadSubmit task = new FileUploadSubmit(); task.setFiles( editor.update() ); return task; } @Override protected String title4PopupWindow() { return "上传demo"; } } |
与下载一样,这里我们只需要注意一个DynamicsPaneUtils.createTablePane(FileDefine.class);的使用即可,
他可以把带@Column和@Config注解且实现了FCloneable的对象,转换成支持公式编辑的表格配置界面(后面可以看实际效果)
然后我们注册到插件中生效即可
Code Block | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
| ||||||||||||
package com.tptj.bridge.hg.file.load.demo; import com.fr.design.beans.BasicBeanPane; import com.fr.design.fun.impl.AbstractSubmitProvider; /** * @author 秃破天际 * @version 10.0 * Created by 秃破天际 on 2021-01-04 **/ public class FileUploadSubmitBridge extends AbstractSubmitProvider { @Override public BasicBeanPane appearanceForSubmit() { return new FileUploadSubmitPane(); } @Override public String dataForSubmit() { return "上传demo"; } @Override public String keyForSubmit() { return FileUploadSubmit.Job_Type; } } |
注意:这里的 keyForSubmit一定要跟下载操作类的getJobType值一致才行。
Code Block | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|
| ||||||||||
<extra-designer> <SubmitProvider class="com.tptj.bridge.hg.file.load.demo.FileUploadSubmitBridge"/> </extra-designer> |
运行效果如下:
注意:这个只是个demo,为了说明当前的接口标准。很多判断和限制是没有在demo中实现的。
最后附上完整的demo代码
View file | ||||
---|---|---|---|---|
|