背景:以往我们对上传和下载类的插件实现没有任何要求,只要求功能实现即可,导致了大家各种奇思妙想的迸发,设计和实现方案迥异。最终导致移动端完全无法适配(每个插件单独适配成本高、耦合重、维护难)

在跟移动端组协商后,我们统一以下的接口标准

移动端那边的适配工作预计在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  

功能:产品内部已有的附件下载接口(我们下载的时候需要借助产品的附件这个入口实现)

响应:文件流


二、实现

因为要根据上面的标准实现下载功能比较麻烦,所以我们在原产品超链接口的基础上封装了一个专门处理下载逻辑的接口给大家使用。这坨代码大多都是内部的实现,使用者可以不用过多了解(当然你喜欢专研的话也可以看一看,只是我们不建议你去看),用的时候直接复制即可

可以直接跳到下面的红色分割线去看后续的内容,接下来的这一大坨可以不用看!

可以直接跳到下面的红色分割线去看后续的内容,接下来的这一大坨可以不用看!

可以直接跳到下面的红色分割线去看后续的内容,接下来的这一大坨可以不用看!

开始就是一个基本的工具方法类,一个是克隆list的,一个是把map的value转换成数组的。

package com.tptj.bridge.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.0
 * Created by 秃破天际 on 2021-01-04
 **/
public class BaseUtils {
    public static <T extends FCloneable> List<T> cloneList(List<T> old )throws CloneNotSupportedException{
        if( null == old ){
            return null;
        }
        List<T> list = new ArrayList<T>();
        for( T item : old ){
            list.add( (T)item.clone() );
        }
        return list;
    }

    public static <T> List<T> getValues( Map<String,T> data ){
        List<T> rt = new ArrayList<T>(data.size());
        Set<Map.Entry<String, T>> entries = data.entrySet();
        for( Map.Entry<String, T> entry : entries ){
            rt.add( entry.getValue() );
        }
        return rt;
    }
}

接下来是我们上传的时候可以绑定多个上传文件任务在一个事件里面,所以我们需要定义表格的编辑器(每一行就是一个文件上传任务)

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();
    }
}

我们不同的上传功能会需要绑定不同的属性,而且这些属性是随着单元格扩展动态计算的,所以我们定义一个文件任务的动态的配置器,可以自己设置属性

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了,没啥特别的,照着实现即可

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接口做进一步的封装,把一些动态计算的产品逻辑和附件功能的使用直接封装起来,使用者就不用关心怎么实现这些了(看最后怎么用即可)

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做进一步的封装

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);
            }
        }
    }

}


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即可

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"
        };
    }
}


/**
 * @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);
                }
            }
        })
    }
})();

接下来是我们定制一个特殊的附件类,来动态的处理我们的下载任务。使用者不需要知道他是如何生效的,使用的时候直接复制

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();
    }
}


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;
    }
}


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;
    }
}

文件长传的时候,我们会把最终要上传的内容数据封装在下面这个对象中告知开发者,同时也会告知这个任务是否是编辑后的,是否是删除

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里面会自动改)

<?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

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(){
        return getValue(KEY_RENAME);
    }

    public Object getRename(){
        return getDynamicsConfig(KEY_RENAME);
    }

    public void setRename( Object rename ){
        setDynamicsConfig(KEY_RENAME,rename);
    }

}

其中我们注意到几个方法

getOutputFilename 就是获取到最终输出文件的用户名(重命名之后的),load方法就是直接获取这个下载连接的文件流,这个例子我们直接从本地磁盘获取

getValue(key) 表示获取配置的一个属性的动态值(比如有单元格扩展,公式引用等等的),最终下载执行的时候才会用到

与之对应的是getDynamicsConfig(key)和setDynamicsConfig(key,value),分别表示获取和设置配置属性,在设计器制作模板的时候会用到(现在看有点懵,先不用管他,后面界面配置代码出来就清楚了)

如果我们除了使用动态配置以外还有自己单独定义的一些配置项,则一定要自己实现clone方法,否则动态计算的时候就没效果了。

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(){
        setLayout(FRGUIPaneFactory.createM_BorderLayout());
        path = ValueEditorPaneFactory.createBasicValueEditorPane();
        file = ValueEditorPaneFactory.createBasicValueEditorPane();
        rename = ValueEditorPaneFactory.createBasicValueEditorPane();
        add(TableLayoutHelper.createTableLayoutPane(
                new Component[][]{
                        //国际化就不单独写了
                        {new UILabel(DownloadHyperlink.KEY_PATH), path},
                        {new UILabel(DownloadHyperlink.KEY_FILE), file},
                        {new UILabel(DownloadHyperlink.KEY_RENAME), rename}
                },
                new double[]{TableLayout.PREFERRED,TableLayout.PREFERRED,TableLayout.PREFERRED},
                new double[]{TableLayout.PREFERRED,TableLayout.FILL}),BorderLayout.CENTER);
    }

    @Override
    public void populateBean( DownloadHyperlink link ) {
        if( null == link ){
            return;
        }
        path.populate( link.getPath() );
        file.populate( link.getFile() );
        rename.populate( link.getRename() );
    }

    @Override
    public DownloadHyperlink updateBean() {
        DownloadHyperlink rt = new DownloadHyperlink();
        rt.setPath( path.update() );
        rt.setFile( file.update() );
        rt.setRename( rename.update() );
        return rt;
    }

    @Override
    protected String title4PopupWindow() {
        return "下载demo";
    }
}

这个配置界面,我们定义了3个配置项,路径/文件/重命名

这里对应的populateBean/updateBean 我们就使用了上面的getDynamicsConfig(key)和setDynamicsConfig(key,value)封装的get和set方法了,通过这些方法实现把配置保存起来和读出来

然后我们注册到插件

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;
    }
}


<extra-designer>
        <HyperlinkProvider class="com.tptj.bridge.hg.file.load.demo.DownloadHyperlinkBridge"/>
</extra-designer>

这样我们的下载功能就实现了

下面我们看上传功能的实现,这个界面就稍微复杂一些了

我们一样的先实现产品的SubmitTask,只是我们这里直接继承上面已经封装好的FileUploadSubmitTask

package com.tptj.bridge.hg.file.load.demo;

import com.fr.cache.Attachment;
import com.fr.io.repository.base.fs.FileSystemRepository;
import com.fr.log.FineLoggerFactory;
import com.fr.stable.StableUtils;
import com.fr.stable.StringUtils;
import com.fr.stable.xml.XMLPrintWriter;
import com.fr.stable.xml.XMLableReader;
import com.tptj.bridge.hg.file.load.core.FileUploadSubmitTask;
import com.tptj.bridge.hg.file.load.core.UploadRowData;
import java.io.InputStream;

/**
 * @author 秃破天际
 * @version 10.0
 * Created by 秃破天际 on 2021-01-04
 **/
public class FileUploadSubmit extends FileUploadSubmitTask {

    @Override
    protected void submit(UploadRowData file_row) {
        file_row.isDelete();
        file_row.isModify();
        try{
            String full_path =StableUtils.pathJoin( getPathValue(file_row),
                    StringUtils.isNotEmpty( getRenameValue(file_row) )? getRenameValue(file_row): getFilename(file_row) );
            FileSystemRepository.getSingleton().write( full_path, getFileValue(file_row));

        }catch(Exception e){
            FineLoggerFactory.getLogger().error(e,e.getMessage());
        }
    }

    private String getPathValue( UploadRowData file_row ){
        return file_row.get(DownloadHyperlink.KEY_PATH);
    }

    private InputStream getFileValue(UploadRowData file_row ){
        Attachment attachment = file_row.get(DownloadHyperlink.KEY_FILE);
        return attachment.getInputStream();
    }

    private String getFilename(UploadRowData file_row ){
        Attachment attachment = file_row.get(DownloadHyperlink.KEY_FILE);
        return attachment.getFilename();
    }

    private String getRenameValue( UploadRowData file_row ){
        return file_row.get(DownloadHyperlink.KEY_RENAME);
    }
    public final static String Job_Type = "upload_demo";

    @Override
    public String getJobType() {
        return Job_Type;
    }

    public final static String XML_TAG = "FileUploadSubmit";

    @Override
    public void readXML( XMLableReader reader ) {
        super.readXML(reader);
    }

    @Override
    public void writeXML( XMLPrintWriter writer ) {
        super.writeXML(writer);
        writer.startTAG(XML_TAG).end();
    }
}


这里我们需要实现的方法有 readXML/writeXML/getJobType/submit是个方法,如果我们除了使用封装好的动态属性之外还有其他配置,一定要自己实现clone方法

这里的UploadRowData 跟我们下载里面的动态配置效果是一样的。在submit中我们读取相关的路径/文件附件/重命名信息保存即可

file_row.isDelete();
file_row.isModify();

这两个方法用来表示 这个数据是否是编辑过的或者是否是删除(删除时一定是编辑,但是是编辑过的不一定是删除),主要用来判断增删改的,当然我们的demo里面只是为了说明接口的使用,就没有实现这些判断

下面我们实现配置界面

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.bridge.hg.file.load.core.BaseUtils;
import com.tptj.bridge.hg.file.load.core.DynamicsConfig;
import com.tptj.bridge.hg.file.load.core.DynamicsConfigModel;

import java.awt.*;
import java.util.HashMap;
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);
        cols.put(DownloadHyperlink.KEY_PATH,"路径");
        cols.put(DownloadHyperlink.KEY_FILE,"文件");
        cols.put(DownloadHyperlink.KEY_RENAME,"重命名");
        w_files = new UITableEditorPane<DynamicsConfig>(new DynamicsConfigModel(cols));
        add(w_files, BorderLayout.CENTER);
    }

    @Override
    public void populateBean( FileUploadSubmit data ) {
        if( null == data || null == data.getFiles() ){
            return;
        }
        w_files.populate( data.getFiles().toArray( new DynamicsConfig[data.getFiles().size()] ) );
    }

    @Override
    public FileUploadSubmit updateBean() {
        FileUploadSubmit task = new FileUploadSubmit();
        try{
            task.setFiles( BaseUtils.cloneList( w_files.update() ) );
        }catch(Exception e){

        }
        return task;
    }

    @Override
    protected String title4PopupWindow() {
        return "上传demo";
    }
}


然后我们注册到插件

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一致

<extra-designer>
        <SubmitProvider class="com.tptj.bridge.hg.file.load.demo.FileUploadSubmitBridge"/>
</extra-designer>

然后就完成了


注意:这个只是个demo,为了说明当前的接口标准。很多判断和限制是没有在demo中实现的。

最后附上完整的代码


最后再次强调:我们实际开发的时候,只需要实现红色分割线以下的这些代码就好了,其他的直接copy一下即可(最多改该路径什么的就可以了)

需要单独开发的:超链对象、超链配置面板、超链注册器、填报对象、填报配置面板、填报注册器。