【仅供内部供应商使用,不提供对外解答和培训】
...
4、所有下载插件的前端必须统一以FR.downloadHyperlink( taskId ) 为入口实现下载触发,且超链接的type属性必须是download
5、所有文件下载的服务必须统一为两个web接口:
FR.servletURL+"?op=fr_attach&cmd=ah_info&id="+taskId
...
功能:产品内部已有的附件下载接口(我们下载的时候需要借助产品的附件这个入口实现)
响应:文件流
因为要根据上面的标准实现下载功能比较麻烦,所以我们在原产品超链接口的基础上封装了一个专门处理下载逻辑的接口给大家使用。这坨代码大多都是内部的实现,使用者可以不用过多了解(当然你喜欢专研的话也可以看一看,只是我们不建议你去看),用的时候直接复制即可
可以直接跳到下面的红色分割线去看后续的内容,接下来的这一大坨可以不用看!
可以直接跳到下面的红色分割线去看后续的内容,接下来的这一大坨可以不用看!
可以直接跳到下面的红色分割线去看后续的内容,接下来的这一大坨可以不用看!
...
首先我们要依赖于一个插件(标准中有两个请求,但是产品里面第一个获取文件信息的接口并没有实现,所以我们单独做了一个补丁插件把这接口和JS的部分都封装好了)
View file | ||||
---|---|---|---|---|
|
安装这个插件除了获得获取文件信息接口的能力外,插件还提供了一些产品内部计算逻辑和简化逻辑的封装,我们开发自己的插件时plugin.xml不要忘记添加依赖哟
Code Block | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
| ||||||||||||
package <dependence> <item type="plugin" key="com.tptj.bridgeextra.hg.file.load.core; import com.fr.stable.FCloneable; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; /** * @author 秃破天际 * @version 10.0attachment.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-0407 **/ public class BaseUtilsFileDefine implements FCloneable { public static <Tfinal extendsString FCloneable>KEY_PATH List<T> cloneList(List<T> old )throws CloneNotSupportedException{ if( null == old ){ return null;= "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; List<T> list = new ArrayList<T>();@Config(KEY_FILE) for@Column(title T item : old ){= "文件",idx = 1) private Object file; @Config(KEY_RENAME) list.add @Column( (T)item.clone() ); title = "重命名",idx = 2) private Object rename; @GetConfig(KEY_PATH) public Object getPath() }{ return listpath; } public static <T> List<T> getValues( Map<String,T> data )void setPath(Object path) { List<T> rtthis.path = new ArrayList<T>(data.size())path; } Set<Map.Entry<String, T>> entries = data.entrySet(); @GetConfig(KEY_FILE) public Object getFile() { return file; } for( Map.Entry<String, T> entrypublic :void entriessetFile(Object file) { this.file = file; rt.add( entry.getValue() );} @GetConfig(KEY_RENAME) public Object getRename() }{ return rtrename; } } |
接下来是我们上传的时候可以绑定多个上传文件任务在一个事件里面,所以我们需要定义表格的编辑器(每一行就是一个文件上传任务)
Code Block | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
| ||||||||||||
package com.tptj.bridge.hg.file.load.core;
import com.fr.design.editor.ValueEditorPane;
import com.fr.design.editor.ValueEditorPaneFactory;
import com.fr.design.editor.editor.Editor;
import com.fr.design.editor.editor.FormulaEditor;
import com.fr.design.editor.editor.TextEditor;
import com.fr.design.i18n.Toolkit;
import com.fr.stable.StringUtils;
import javax.swing.*;
import javax.swing.table.TableCellEditor;
import java.awt.Component;
/**
* @author 秃破天际
* @version 10.0
* Created by 秃破天际 on 2021-01-04
**/
public class CellEditor extends AbstractCellEditor implements TableCellEditor {
private ValueEditorPane editor;
public CellEditor() {
editor = ValueEditorPaneFactory.createValueEditorPane(new Editor[]{
new TextEditor(),
new FormulaEditor(Toolkit.i18nText("Fine-Design_Basic_Parameter_Formula"))
}, StringUtils.EMPTY, StringUtils.EMPTY);
}
@Override
public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
editor.populate( value );
return editor;
}
@Override
public Object getCellEditorValue() {
return editor.update();
}
} |
我们不同的上传功能会需要绑定不同的属性,而且这些属性是随着单元格扩展动态计算的,所以我们定义一个文件任务的动态的配置器,可以自己设置属性
Code Block | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
| ||||||||||||
package com.tptj.bridge.hg.file.load.core;
import com.fr.base.BaseFormula;
import com.fr.base.BaseXMLUtils;
import com.fr.base.Parameter;
import com.fr.stable.FCloneable;
import com.fr.stable.ParameterProvider;
import com.fr.stable.xml.StableXMLUtils;
import com.fr.stable.xml.XMLPrintWriter;
import com.fr.stable.xml.XMLable;
import com.fr.stable.xml.XMLableReader;
import java.util.*;
/**
* @author 秃破天际
* @version 10.0
* Created by 秃破天际 on 2021-01-04
**/
public class DynamicsConfig implements XMLable {
public final static String XML_TAG = "DynamicsConfig";
private Map<String, Object> config = new HashMap<String, Object>(0);
public void put( String key, Object value ){
config.put( key,value );
}
@Override
public Object clone() throws CloneNotSupportedException {
DynamicsConfig item = (DynamicsConfig)super.clone();
Set<Map.Entry<String, Object>> entries = config.entrySet();
item.config = new HashMap<String, Object>(config.size());
for( Map.Entry<String, Object> entry : entries ){
Object val = entry.getValue();
if( val instanceof FCloneable){
val = ((FCloneable)val).clone() ;
}
item.config.put( entry.getKey(), val );
}
return item;
}
@Override
public void readXML(XMLableReader reader) {
String tag = reader.getTagName();
boolean child = reader.isChildNode();
if ( XML_TAG.equals( tag ) ) {
Parameter[] params = BaseXMLUtils.readParameters(reader);
config = new HashMap<String, Object>(params.length);
for( Parameter param : params ){
config.put(param.getName(),param.getValue());
}
}
}
@Override
public void writeXML(XMLPrintWriter writer) {
writer.startTAG(XML_TAG);
ParameterProvider[] execute = get4execute();
for( ParameterProvider param : execute ){
StableXMLUtils.writeParameter(writer,param);
}
writer.end();
}
protected ParameterProvider[] get4execute(){
Set<Map.Entry<String, Object>> entries = config.entrySet();
List<ParameterProvider> list = new ArrayList<ParameterProvider>(config.size());
for( Map.Entry<String, Object> entry : entries ){
String key = entry.getKey();
Object val = entry.getValue();
list.add( new Parameter(key,val) );
}
return list.toArray(new Parameter[0]);
}
public Object get(String key) {
return config.get(key);
}
public Set<String> keySet() {
return config.keySet();
}
public <T> T getValue( String key ){
Object rt = config.get( key );
if( null == rt ){
return null;
}
if( rt instanceof BaseFormula){
return (T)((BaseFormula)rt).getResult();
}
return (T)rt;
}
}
|
下面这个就是表格的model了,没啥特别的,照着实现即可
Code Block | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
| ||||||||||||
package com.tptj.bridge.hg.file.load.core;
import com.fr.design.gui.itableeditorpane.UITableEditAction;
import com.fr.design.gui.itableeditorpane.UITableModelAdapter;
import java.awt.event.ActionEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* @author 秃破天际
* @version 10.0
* Created by 秃破天际 on 2021-01-04
**/
public class DynamicsConfigModel extends UITableModelAdapter<DynamicsConfig> {
private List<String> keys;
public DynamicsConfigModel( Map<String,String> configs ) {
super(BaseUtils.getValues( configs ).toArray(new String[configs.size()]));
keys = new ArrayList<String>(configs.keySet());
Class [] editor_class = new Class[configs.size()];
for( int i=0,len=configs.size(); i<len; i++ ){
editor_class[i] = CellEditor.class;
}
this.setColumnClass( editor_class );
this.setDefaultEditor(CellEditor.class, new CellEditor());
}
@Override
public void setValueAt(Object val, int r_idx, int c_idx ) {
DynamicsConfig row = getList().get(r_idx);
row.put( keys.get(c_idx), val);
}
@Override
public Object getValueAt(int r_idx, int c_idx) {
DynamicsConfig row = getList().get(r_idx);
return row.get( keys.get(c_idx) );
}
@Override
public boolean isCellEditable(int r_idx, int c_idx) {
return true;
}
@Override
public UITableEditAction[] createAction() {
return new UITableEditAction[]{ new AddFileAction(),new DeleteAction(), new MoveUpAction(), new MoveDownAction() };
}
private class AddFileAction extends AddTableRowAction {
@Override
public void actionPerformed(ActionEvent e) {
super.actionPerformed(e);
addRow(new DynamicsConfig());
fireTableDataChanged();
table.getSelectionModel().setSelectionInterval(table.getRowCount() - 1, table.getRowCount() - 1);
}
}
} |
上面都是准备,接下来我们对Hyperlink接口做进一步的封装,把一些动态计算的产品逻辑和附件功能的使用直接封装起来,使用者就不用关心怎么实现这些了(看最后怎么用即可)
Code Block | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
| ||||||||||||
package com.tptj.bridge.hg.file.load.core;
import com.fr.cache.AttachmentSource;
import com.fr.js.Hyperlink;
import com.fr.stable.ArrayUtils;
import com.fr.stable.ParameterProvider;
import com.fr.stable.StringUtils;
import com.fr.stable.web.Repository;
import com.fr.stable.xml.XMLPrintWriter;
import com.fr.stable.xml.XMLableReader;
import com.fr.web.core.SessionPoolManager;
import com.fr.web.core.TemplateSessionIDInfo;
import java.io.InputStream;
import java.util.*;
/**
* @author 秃破天际
* @version 10.0
* Created by 秃破天际 on 2021-01-04
**/
public abstract class FileDownloadHyperlink extends Hyperlink {
public final static String XML_TAG = "FileDownloadHyperlink";
private DynamicsConfig config = new DynamicsConfig();
/**
* 配置不变的时候是否要强制下载
*/
private boolean force = true;
public boolean isForce() {
return force;
}
public void setForce(boolean force) {
this.force = force;
}
public Set<String> configKeySet(){
return config.keySet();
}
public void setDynamicsConfig(String key, Object value ){
config.put(key,value);
}
public Object getDynamicsConfig( String key ){
return config.get( key );
}
public <T> T getValue( String key ){
return config.getValue(key);
}
@Override
public ParameterProvider[] getExtraParameterizedConfig(){
ParameterProvider[] config = super.getExtraParameterizedConfig();
return ArrayUtils.addAll( this.config.get4execute(), config );
}
@Override
public Object clone() throws CloneNotSupportedException {
FileDownloadHyperlink item = (FileDownloadHyperlink)super.clone();
item.config = (DynamicsConfig) config.clone();
item.force = force;
return item;
}
@Override
public void readXML(XMLableReader reader) {
super.readXML(reader);
String tag = reader.getTagName();
if ( XML_TAG.equals( tag ) ) {
force = reader.getAttrAsBoolean("force",true);
}else if( DynamicsConfig.XML_TAG.equals( tag ) ){
config = new DynamicsConfig();
reader.readXMLObject( config );
}
}
@Override
public void writeXML(XMLPrintWriter writer) {
super.writeXML(writer);
writer.startTAG(XML_TAG).attr("force",force);
if( null != config ){
config.writeXML(writer);
}
writer.end();
}
@Override
protected String actionJS( Repository repository ) {
StringBuilder sb = new StringBuilder();
sb.append("FR.downloadHyperlink(\"")
.append(getCode(repository))
.append("\")");
return sb.toString();
}
/**
* 克隆的时候不要克隆这个值,这个只是用来防止反复重复生成附件用的
*/
private String code = null;
private String getCode( Repository repository ){
if( StringUtils.isNotEmpty( code ) ){
return code;
}
String sessionID = repository.getSessionID();
TemplateSessionIDInfo info = SessionPoolManager.getSessionIDInfor(sessionID, TemplateSessionIDInfo.class);
if ( null == info) {
return StringUtils.EMPTY;
}
try {
code = UUID.randomUUID().toString().replaceAll("-","") + System.currentTimeMillis() + sessionID;
SpAttachmentFileBase file_base = new SpAttachmentFileBase( code,this );
SpAttachment attach = new SpAttachment(code, file_base);
AttachmentSource.putAttachment(code, attach);
info.registerAttachmentID(code);
}catch(Exception e){
code = null;
}
return code;
}
/**
* 返回最终下载文件的用户名
* @return
*/
public abstract String getOutputFilename();
/**
* 获取文件流
* @return 如果文件不存在或者有其他异常,返回null
*/
public abstract InputStream load();
} |
然后是我们的上传任务接口同样对AbstractSubmitTask做进一步的封装
Code Block | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
| ||||||||||||
package com.tptj.bridge.hg.file.load.core;
import com.fr.data.AbstractSubmitTask;
import com.fr.script.Calculator;
import com.fr.stable.ParameterProvider;
import com.fr.stable.xml.XMLPrintWriter;
import com.fr.stable.xml.XMLableReader;
import com.fr.write.DMLReport;
import com.fr.write.config.ColumnConfig;
import com.fr.write.config.DMLConfig;
import com.fr.writex.collect.RowDataCollector;
import com.fr.writex.data.RowDataEntry;
import java.util.ArrayList;
import java.util.List;
/**
* @author 秃破天际
* @version 10.0
* Created by 秃破天际 on 2021-01-04
**/
public abstract class FileUploadSubmitTask extends AbstractSubmitTask {
private List<DynamicsConfig> files = new ArrayList<DynamicsConfig>(0);
public List<DynamicsConfig> getFiles() {
return files;
}
public void setFiles(List<DynamicsConfig> files) {
this.files = files;
}
@Override
public void doJob( Calculator calculator ) throws Exception {
DMLConfig old = calculator.getAttribute(DMLConfig.KEY);
DMLReport report = calculator.getAttribute(DMLReport.KEY);
for( DynamicsConfig file : files ){
submit( report, file, calculator );
}
if( null != old ){
calculator.setAttribute( DMLConfig.KEY,old );
}
}
private void submit( DMLReport report, DynamicsConfig file, Calculator calculator )throws Exception{
DMLConfig crt = new DMLConfig(){};
ParameterProvider[] conf_row = file.get4execute();
for( ParameterProvider item : conf_row ){
crt.addColumnConfig( new ColumnConfig( item.getName(), item.getValue(), false ));
}
calculator.setAttribute( DMLConfig.KEY, crt );
RowDataCollector ctrl = report.getGroupRowDataCollector(calculator, null);
List<RowDataEntry> rows = ctrl.collectData();
for( RowDataEntry row : rows ){
UploadRowData file_row =new UploadRowData(row).build(conf_row);
submit(file_row);
}
}
/**
* 真正文件处理的地方(增删改)
* @param file_row
*/
protected abstract void submit( UploadRowData file_row );
@Override
public void doFinish( Calculator calculator ) throws Exception {
}
@Override
public Object clone() throws CloneNotSupportedException {
FileUploadSubmitTask obj = (FileUploadSubmitTask) super.clone();
obj.files = BaseUtils.cloneList( files );
return obj;
}
@Override
public void readXML( XMLableReader reader ) {
String tag = reader.getTagName();
boolean isc = reader.isChildNode();
if( DynamicsConfig.XML_TAG.equals( tag ) ){
DynamicsConfig item = new DynamicsConfig();
reader.readXMLObject( item );
files.add( item );
}
}
@Override
public void writeXML( XMLPrintWriter writer ) {
if(null != files && !files.isEmpty()){
for( DynamicsConfig file : files ){
file.writeXML(writer);
}
}
}
} |
Code Block | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
| ||||||||||||
package com.tptj.bridge.hg.file.load.core;
import com.fr.cache.Attachment;
import com.fr.cache.AttachmentSource;
import com.fr.decision.fun.impl.AbstractGlobalRequestFilterProvider;
import com.fr.json.JSONObject;
import com.fr.log.FineLoggerFactory;
import com.fr.stable.StringUtils;
import com.fr.web.utils.WebUtils;
import com.tptj.bridge.hg.file.load.core.SpAttachment;
import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author 秃破天际
* @version 10.0
* Created by 秃破天际 on 2021-01-04
**/
public class Filter extends AbstractGlobalRequestFilterProvider {
public static final String RESPONSE_TAG = "_download_response_";
@Override
public String filterName() {
return "Download Filter";
}
@Override
public String[] urlPatterns() {
return new String[]{"/*"};
}
@Override
public void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain) {
String op = WebUtils.getHTTPRequestParameter(req,"op");
if( !StringUtils.equals("fr_attach",op) ){
noFilter(req,res,chain);
return;
}
String cmd = WebUtils.getHTTPRequestParameter(req,"cmd");
if( StringUtils.equals("ah_info",cmd ) ){
//下载前获取文件信息
JSONObject data = JSONObject.create();
try{
String id= WebUtils.getHTTPRequestParameter(req,"id");
Attachment attachment = AttachmentSource.getAttachment(id);
if( attachment instanceof SpAttachment){
data.put("size",((SpAttachment)attachment).getLength() );
}else{
data.put("size",attachment.getInputStream().available() );
}
data.put("filename",attachment.getFilename());
data.put("id",id);
data.put("success",true);
}catch(Exception e){
data.put("error_msg",e.getMessage());
data.put("success",false);
FineLoggerFactory.getLogger().error(e,e.getMessage());
}
try{
WebUtils.flushSuccessMessageAutoClose(req,res,data);
}catch(Exception e){
FineLoggerFactory.getLogger().error(e,e.getMessage());
}
return;
}
noFilter(req,res,chain);
}
private void noFilter(HttpServletRequest req, HttpServletResponse res, FilterChain chain){
try{
chain.doFilter(req,res);
}catch(Exception e){ }
}
} |
因为我们下载的前端是个超链,所以要统一按照上面的基本原则封装一个下载的方法,使用者无需单独封装,直接copy即可
Code Block | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
| ||||||||||||
package com.tptj.bridge.hg.file.load.core;
import com.fr.stable.fun.impl.AbstractJavaScriptFileHandler;
/**
* @author 秃破天际
* @version 10.0
* Created by 秃破天际 on 2021-01-04
**/
public class JavaScriptBridge extends AbstractJavaScriptFileHandler {
@Override
public String[] pathsForFiles() {
return new String[]{
"/com/tptj/bridge/hg/file/load/js/main.js"
};
}
} |
Code Block | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
| ||||||||||||
/**
* @author 秃破天际
* @version 10.0
* Created by 秃破天际 on 2021-01-04
**/
!(function () {
FR.downloadHyperlink = function (id) {
FR.ajax({
url:FR.servletURL+"?op=fr_attach&cmd=ah_info&id="+id,
success:function ( res ) {
res = FR.jsonDecode(res)
if( res.success ){
window.open(FR.servletURL+"?op=fr_attach&cmd=ah_download&id="+id);
}else{
alert(res.error_msg);
}
}
})
}
})(); |
接下来是我们定制一个特殊的附件类,来动态的处理我们的下载任务。使用者不需要知道他是如何生效的,使用的时候直接复制
Code Block | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
| ||||||||||||
package com.tptj.bridge.hg.file.load.core;
import com.fr.cache.Attachment;
/**
* @author 秃破天际
* @version 10.0
* Created by 秃破天际 on 2021-01-04
**/
public class SpAttachment extends Attachment {
private SpAttachmentFileBase base;
public SpAttachment(String id, SpAttachmentFileBase base ){
super(id,"other","",base);
this.base = base;
}
@Override
public String getFilename(){
return base.getFileName();
}
public int getLength(){
return base.getLength();
}
} |
Code Block | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
| ||||||||||||
package com.tptj.bridge.hg.file.load.core;
import com.fr.cache.AttachmentFileBase;
import com.fr.cache.concept.AttachmentFileProvider;
import com.fr.general.ComparatorUtils;
import com.fr.plugin.transform.ExecuteFunctionRecord;
import com.fr.plugin.transform.FunctionRecorder;
import com.fr.stable.StringUtils;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
/**
* @author 秃破天际
* @version 10.0
* Created by 秃破天际 on 2021-01-04
**/
@FunctionRecorder
public class SpAttachmentFileBase extends AttachmentFileBase {
private FileDownloadHyperlink link;
public SpAttachmentFileBase( String id, FileDownloadHyperlink link ) {
super(AttachmentFileProvider.DEFAULT_REPO, id);
this.link = link;
}
@Override
public String getFileName() {
if( null == cache ){
return StringUtils.EMPTY;
}
return link.getOutputFilename();
}
public int getLength(){
if( !load() ){
return 0;
}
return cache.getLength();
}
@Override
@ExecuteFunctionRecord
public InputStream getInput() {
if( null == cache ){
return null;
}
return cache.reloadCache();
}
private StoreInputStream cache = null;
private boolean load(){
Map<String,Object> config = getStoreConfig();
if( link.isForce() || !sameConfig( config ) ){
InputStream in = link.load();
if( null != in ){
cache = new StoreInputStream(in);
crt_config = config;
return true;
}else{
return false;
}
}else{
return null != cache;
}
}
private Map<String,Object> crt_config = new HashMap<String,Object>(0);
private boolean sameConfig( Map<String,Object> config ){
if( crt_config.size() != config.size() ){
return false;
}
Iterator<Map.Entry<String, Object>> iterator = crt_config.entrySet().iterator();
while (iterator.hasNext()){
Map.Entry<String, Object> next = iterator.next();
if( !ComparatorUtils.equals( next.getValue(), config.get( next.getKey() ) ) ){
return false;
}
}
return true;
}
private Map<String,Object> getStoreConfig(){
Set<String> set = link.configKeySet();
Map<String,Object> cfg = new HashMap<String,Object>(set.size());
for( String key : set ){
cfg.put( key, link.getValue(key) );
}
return cfg;
}
} |
Code Block | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
| ||||||||||||
package com.tptj.bridge.hg.file.load.core;
import com.fr.general.IOUtils;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
/**
* @author 秃破天际
* @version 10.0
* Created by 秃破天际 on 2021-01-04
**/
public class StoreInputStream {
private byte[] data = new byte[0];
public StoreInputStream( InputStream in ){
data = IOUtils.inputStream2Bytes(in);
}
public InputStream reloadCache(){
return new ByteArrayInputStream( data );
}
public int getLength(){
return data.length;
}
} |
文件长传的时候,我们会把最终要上传的内容数据封装在下面这个对象中告知开发者,同时也会告知这个任务是否是编辑后的,是否是删除
Code Block | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
| ||||||||||||
package com.tptj.bridge.hg.file.load.core;
import com.fr.stable.ParameterProvider;
import com.fr.writex.data.RowDataEntry;
import java.util.HashMap;
import java.util.Map;
/**
* @author 秃破天际
* @version 10.0
* Created by 秃破天际 on 2021-01-04
**/
public class UploadRowData {
private Map<String,Object> data;
private RowDataEntry row;
public UploadRowData( RowDataEntry row ) {
this.row = row;
}
protected UploadRowData build( ParameterProvider[] conf_row ){
data = new HashMap<String,Object>(conf_row.length);
for( int i=0,len=conf_row.length; i<len; i++ ){
data.put( conf_row[i].getName(), row.getColumnValues()[i] );
}
return this;
}
public <T> T get( String key ){
return (T)data.get(key);
}
public boolean isDelete(){
return row.checkDoDelete();
}
public boolean isModify(){
return row.checkModified();
}
} |
下面是上面用到的插件接口的基本注册,如果你自己修改了包路径的话自己对应的改这里的路径即可(IDEA里面会自动改)
Code Block | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
| ||||||||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?><plugin>
<id>com.tptj.bridge.hg.file.load</id>
<name><![CDATA[ 文件上传下载demo ]]></name>
<active>yes</active>
<version>1.0</version>
<env-version>10.0</env-version>
<vendor>tptj</vendor>
<jartime>2019-07-18</jartime>
<description><![CDATA[ ]]></description>
<change-notes/>
<main-package>com.tptj.bridge.hg.file.load</main-package>
<function-recorder class="com.tptj.bridge.hg.file.load.core.SpAttachmentFileBase"/>
<extra-report>
<JavaScriptFileHandler class="com.tptj.bridge.hg.file.load.core.JavaScriptBridge"/>
</extra-report>
<extra-decision>
<GlobalRequestFilterProvider class="com.tptj.bridge.hg.file.load.core.Filter"/>
</extra-decision>
</plugin> |
======================================没错!我就是红色分割线===================================
以上的这坨代码是把大家不太会写的逻辑都先写好了,下面我们看一个demo代码(主要就是开发配置界面的东西多一些)
下载比较简单我们先看下载的部分:
首先是实现一个Hyperlink和对应的配置界面,其中Hyperlink我们直接集成上面这坨代码中的FileDownloadHyperlink
Code Block | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|
| ||||||||||
package com.tptj.bridge.hg.file.load.demo; import com.fr.stable.StableUtils; import com.fr.stable.StringUtils; import com.tptj.bridge.hg.file.load.core.FileDownloadHyperlink; import java.io.FileInputStream; import java.io.InputStream; /** * @author 秃破天际 * @version 10.0 * Created by 秃破天际 on 2021-01-04 **/ public class DownloadHyperlink extends FileDownloadHyperlink { public final static String KEY_PATH = "path"; public final static String KEY_FILE = "file"; public final static String KEY_RENAME = "rename"; @Override public String getOutputFilename() { String val = getRenameValue(); if( StringUtils.isEmpty(val) ){ val = getFileValue(); } return val; } @Override public InputStream load() { try{ return new FileInputStream( StableUtils.pathJoin( getPathValue(), getFileValue() ) ); }catch(Exception e){ } return null; } private String getPathValue(){ return getValue(KEY_PATH); } public Object getPath(){ return getDynamicsConfig(KEY_PATH); } public void setPath( Object path ){ setDynamicsConfig(KEY_PATH,path); } private String getFileValue(){ return getValue(KEY_FILE); } public Object getFile(){ return getDynamicsConfig(KEY_FILE); } public void setFile( Object file ){ setDynamicsConfig(KEY_FILE,file); } private String getRenameValue() 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 getValue(KEY_RENAME)file; } public Objectvoid getRenamesetFile(FileDefine file) { this.file return getDynamicsConfig(KEY_RENAME)= file; } public@Override void setRename( Objectpublic renameString getOutputFilename() { String setDynamicsConfig(KEY_RENAME,renameval = DynamicsConfigUtils.getConfig( file.getRename() ); } } |
其中我们注意到几个方法
getOutputFilename 就是获取到最终输出文件的用户名(重命名之后的),load方法就是直接获取这个下载连接的文件流,这个例子我们直接从本地磁盘获取
getValue(key) 表示获取配置的一个属性的动态值(比如有单元格扩展,公式引用等等的),最终下载执行的时候才会用到
与之对应的是getDynamicsConfig(key)和setDynamicsConfig(key,value),分别表示获取和设置配置属性,在设计器制作模板的时候会用到(现在看有点懵,先不用管他,后面界面配置代码出来就清楚了)
如果我们除了使用动态配置以外还有自己单独定义的一些配置项,则一定要自己实现clone方法,否则动态计算的时候就没效果了。
Code Block | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|
| ||||||||||
package com.tptj.bridge.hg.file.load.demo; import com.fr.design.beans.BasicBeanPane; import com.fr.design.editor.ValueEditorPane; import com.fr.design.editor.ValueEditorPaneFactory; import com.fr.design.gui.ilable.UILabel; import com.fr.design.layout.FRGUIPaneFactory; import com.fr.design.layout.TableLayout; import com.fr.design.layout.TableLayoutHelper; import java.awt.*; /** * @author 秃破天际 * @version 10.0 * Created by 秃破天际 on 2021-01-04 **/ public class DownloadHyperlinkPane extends BasicBeanPane<DownloadHyperlink> { private ValueEditorPane path; private ValueEditorPane file; private ValueEditorPane rename; public DownloadHyperlinkPane(){ 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){ setLayout(FRGUIPaneFactory.createM_BorderLayout());} path = ValueEditorPaneFactory.createBasicValueEditorPane();return null; } @Override filepublic =Object ValueEditorPaneFactory.createBasicValueEditorPaneclone(); throws CloneNotSupportedException { DownloadHyperlink renameitem = ValueEditorPaneFactory.createBasicValueEditorPane(DownloadHyperlink)super.clone(); add(TableLayoutHelper.createTableLayoutPane(try{ new Component[][]{ item.file = ( FileDefine )file.clone(); }catch(Exception e){ //国际化就不单独写了FineLoggerFactory.getLogger().error(e,e.getMessage()); } return item; {new UILabel(DownloadHyperlink.KEY_PATH), path}, {new UILabel(DownloadHyperlink.KEY_FILE), file}, {new UILabel(DownloadHyperlink.KEY_RENAME), rename} }, } } |
这里我们声明一个我们定义的文件描述对象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(){ new double[]{TableLayout.PREFERRED,TableLayout.PREFERRED,TableLayout.PREFERRED}, setLayout(FRGUIPaneFactory.createM_BorderLayout()); editor = DynamicsPaneUtils.createListPane(FileDefine.class); new double[]{TableLayout.PREFERRED,TableLayout.FILL}),add( editor, BorderLayout.CENTER ); } @Override public void populateBean( DownloadHyperlink link ) { if( null == link ){ { return; if( null == link ){ } path.populate( link.getPath() )return; file.populate( link.getFile() );} renameeditor.populatepopulateBean( link.getRenamegetFile() ); } @Override public DownloadHyperlink updateBean() { DownloadHyperlink rt = new DownloadHyperlink(); rt.setPath( path.update() ); rt.setFile( file.update() ); rt.setRename( rename.updateeditor.updateBean() ); return rt; } @Override protected String title4PopupWindow() { return "下载demo"; } } |
这个配置界面,我们定义了3个配置项,路径/文件/重命名
这里对应的populateBean/updateBean 我们就使用了上面的getDynamicsConfig(key)和setDynamicsConfig(key,value)封装的get和set方法了,通过这些方法实现把配置保存起来和读出来
...
}
} |
这个就是一般的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> |
这样我们的下载功能就实现了运行效果如下:
下面我们看上传功能的实现,这个界面就稍微复杂一些了接下来我们再来实现对应的上传功能,下载我们每次只能添加一个文件任务(没有做文件夹压缩,感兴趣的同学可以自己在把压缩实现一下即可),那么上传我们希望一次可以多个任务。
我们一样的先实现产品的SubmitTask,只是我们这里直接继承上面已经封装好的FileUploadSubmitTask首先我们定义上传的操作类,这里我们也封装好了一个接口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.FineLoggerFactory; import com.fr.plugin.transform.ExecuteFunctionRecord; import com.fr.plugin.stabletransform.StableUtilsFunctionRecorder; import com.fr.stable.StringUtilsPrimitive; import com.fr.stable.xml.XMLPrintWriterStableUtils; import com.fr.stable.xml.XMLableReaderStringUtils; import com.tptj.bridgetools.hg.file.loadoperator.corebridge.FileUploadSubmitTask; import com.tptj.bridgetools.hg.file.loadoperator.coredynamics.UploadRowData; import java.io.InputStream; /** * @author 秃破天际 * @version 10.0 * Created by 秃破天际 on 2021-01-04 **/ @FunctionRecorder public class FileUploadSubmit extends FileUploadSubmitTaskFileUploadSubmitTask<FileDefine> { public final static String Job_Type = "upload_demo"; @Override @ExecuteFunctionRecord protected void submit(UploadRowData file_row) { UploadRowData<FileDefine> file ) { file.isDelete();//这条数据是否属于被删除的数据(删除行列) file_row.isDeleteisModify();//这条数据是否是(单元格)编辑过的数据 FileDefine conf = file_row.isModifygetRowData(FileDefine.class); try{ StringObject full_pathval =StableUtils.pathJoin( getPathValue(file_row), conf.getFile(); StringUtils.isNotEmpty( getRenameValue(file_row) )? getRenameValue(file_row): getFilename(file_row) ); if( !(val instanceof Attachment) ){ FileSystemRepository.getSingleton().write( full_path, getFileValue(file_row)); return; }catch(Exception e){ Attachment attachment = FineLoggerFactory.getLogger().error(e,e.getMessage())Attachment) val; } } val = conf.getPath(); private String getPathValue( UploadRowData file_row ){ String path = return file_row.get(DownloadHyperlink.KEY_PATH)StringUtils.EMPTY; } private InputStreamif( getFileValue!(UploadRowData file_rowval instanceof Primitive) ){ Attachment attachment path = file_rowGeneralUtils.getobjectToString(DownloadHyperlink.KEY_FILE conf.getPath() ); return attachment.getInputStream(); } } private String getFilename(UploadRowData file_row ){ val = conf.getRename(); Attachment String attachmentrename = file_row.get(DownloadHyperlink.KEY_FILE)StringUtils.EMPTY; return attachment.getFilename(); } if( !(val instanceof Primitive) ){ private String getRenameValue( UploadRowData file_row ){ rename return= file_rowGeneralUtils.getobjectToString(DownloadHyperlink.KEY_RENAME conf.getRename() ); } public final static String Jobfull_Typepath = "upload_demo"; StableUtils.pathJoin( path , @Override public String getJobType() { return Job_Type; } StringUtils.isNotEmpty( rename )? rename: attachment.getFilename() ); public final static String XML_TAG = "FileUploadSubmit"; @Override FileSystemRepository.getSingleton().write( full_path, attachment.getInputStream() ); public void readXML( XMLableReader reader}catch(Exception e) { super.readXML(readerFineLoggerFactory.getLogger().error(e,e.getMessage()); } @Override} public} void writeXML( XMLPrintWriter writer ) {@Override public String super.writeXMLgetJobType(writer); { writer.startTAG(XML_TAG).end()return Job_Type; } } |
这里我们需要实现的方法有 readXML/writeXML/getJobType/submit是个方法,如果我们除了使用封装好的动态属性之外还有其他配置,一定要自己实现clone方法
这里的UploadRowData 跟我们下载里面的动态配置效果是一样的。在submit中我们读取相关的路径/文件附件/重命名信息保存即可
file_row.isDelete();
file_row.isModify();
这两个方法用来表示 这个数据是否是编辑过的或者是否是删除(删除时一定是编辑,但是是编辑过的不一定是删除),主要用来判断增删改的,当然我们的demo里面只是为了说明接口的使用,就没有实现这些判断
下面我们实现配置界面
接口实现我们指明了使用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.gui.itableeditorpane.UITableEditorPane; import com.fr.design.layout.FRGUIPaneFactory; import com.tptj.bridgetools.hg.file.load.core.BaseUtils; import com.tptj.bridge.hg.file.load.core.DynamicsConfigoperator.design.DynamicsTablePane; import com.tptj.bridgetools.hg.file.loadoperator.coreutils.DynamicsConfigModelDynamicsPaneUtils; import java.awt.*; import java.util.HashMapList; import java.util.Map; /** * @author 秃破天际 * @version 10.0 * Created by 秃破天际 on 2021-01-04 **/ public class FileUploadSubmitPane extends BasicBeanPane<FileUploadSubmit> { private UITableEditorPane<DynamicsConfig> w_files; public FileUploadSubmitPane() { setLayout(FRGUIPaneFactory.createBorderLayout()); Map<String,String> cols = new HashMap<String,String>(3);.0 * Created by 秃破天际 on 2021-01-04 **/ public class FileUploadSubmitPane extends BasicBeanPane<FileUploadSubmit> { private cols.put(DownloadHyperlink.KEY_PATH,"路径")DynamicsTablePane<FileDefine> editor; cols.put(DownloadHyperlink.KEY_FILE,"文件");public FileUploadSubmitPane() { cols.putsetLayout(DownloadHyperlinkFRGUIPaneFactory.KEY_RENAME,"重命名"createM_BorderLayout()); w_files = neweditor UITableEditorPane<DynamicsConfig>(new DynamicsConfigModel(cols)= DynamicsPaneUtils.createTablePane(FileDefine.class); add(w_files editor, BorderLayout.CENTER ); } @Override public void populateBean( FileUploadSubmit data ) { if( null == data || null == ){ return; } List<FileDefine> files = data.getFiles(); if( null == files ){ return; } w_fileseditor.populate( data.getFiles()files.toArray( new DynamicsConfig[data.getFiles()FileDefine[files.size()] ) ); } @Override public FileUploadSubmit updateBean() { FileUploadSubmit task = new FileUploadSubmit(); try{ task.setFiles( BaseUtils.cloneList( w_fileseditor.update() ) ); }catch(Exception e){ } 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 一定要跟task类的getJobType一致注意:这里的 keyForSubmit一定要跟下载操作类的getJobType值一致才行。
Code Block | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|
| ||||||||||
<extra-designer> <SubmitProvider class="com.tptj.bridge.hg.file.load.demo.FileUploadSubmitBridge"/> </extra-designer> |
然后就完成了运行效果如下:
注意:这个只是个demo,为了说明当前的接口标准。很多判断和限制是没有在demo中实现的。
最后附上完整的代码最后附上完整的demo代码
View file | ||||
---|---|---|---|---|
|
最后再次强调:我们实际开发的时候,只需要实现红色分割线以下的这些代码就好了,其他的直接copy一下即可(最多改该路径什么的就可以了)
需要单独开发的:超链对象、超链配置面板、超链注册器、填报对象、填报配置面板、填报注册器。