基于freemaker的自动化word文档导出

本文档介绍了如何使用FreeMarker处理Word模板,生成Word、Excel和PDF文档。通过解析Word模板中的参数,进行占位符校正、列表参数处理、图片参数处理,然后将XML文档转换为FTL模板,最终利用FreeMarker生成最终文档。文章详细展示了整个流程,包括关键代码实现和使用责任链模式来组织处理步骤。
摘要由CSDN通过智能技术生成

在项目中使用freemaker生成word、excel、pdf文档是比较方便的实现方式,但是也存在一个问题,我们的产品最终是要给客户使用的,而freemaker使用的是ftl模板,而不是word、excel这样的原始文件模板,但对于客户来讲,手动去调整生成ftl模板并不现实,所以我实现了一个word文档自动导出的功能,提供一种解决问题的思路。

一、导包
<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
    <version>2.3.28</version>
</dependency>
<dependency>
    <groupId>dom4j</groupId>
    <artifactId>dom4j</artifactId>
    <version>1.6.1</version>
</dependency>
二、实现对word模板的参数解析,参数校正,列表参数处理,图片参数处理等,以及ftl模板生成

1、解析参数,因为word本质上是以xml文档的格式实现文档的数据存储和处理的,所以我们通过对xml文档的处理实现自动化,第一步是将模板中出现的参数解析出来

import com.tss.mangosercivea.manager.template.DocumentModel;
import com.tss.mangosercivea.manager.template.enums.ParamCategory;
import com.tss.mangosercivea.manager.template.TemplateParam;
import com.tss.mangosercivea.manager.template.XMLHandler;
import org.dom4j.Element;

import java.util.ArrayList;
import java.util.List;
import java.util.Stack;
import java.util.regex.Pattern;

/**
 * 解析xml文档中的参数值
 * <p>
 * Created by yangxiangjun on 2021/1/22.
 */
public class AnalysisParamHandler extends XMLHandler {

    @Override
    public void run(DocumentModel document) {
        List<TemplateParam> params = new ArrayList<>();
        List<Element> nodes = document.getNodes();
        //获取所有的文本信息节点
        Stack stack = new Stack<>();
        boolean isAnalysis = false;
        boolean isStack = false;
        for (Element node : nodes) {
            String path = node.getUniquePath();
            //获取节点的文本信息
            String text = node.getText();
            //如果文本信息包含占位符,则该文本信息后面讲出现与之对应的参数
            for (ParamCategory value : ParamCategory.values()) {
                isAnalysis = isAnalysis || text.contains(value.getParamCate()[0]) || isStack;
            }

            // 如果flag变量为ture,代表当前正在寻找被xml格式转换打乱的参数,将接下来的文本信息入栈,
            // 并且判断是否找到了占位符结束标志,如果找到,代表找到参数,isAnalysis设置为false
            // 此时开启对文本信息的逐个字符读取
            if (isAnalysis) {

                ParamCategory paramCate = null;
                for (int i = 0; i < text.length(); i++) {
                    if (!isStack) {
                        char[] chars = new char[2];
                        chars[0] = text.charAt(i);
                        chars[1] = text.charAt(i + 1);
                        String str = new String(chars);
                        //判断字符串在遍历中是否找到了占位符的开始标志
                        for (ParamCategory value : ParamCategory.values()) {
                            if (str.equals(value.getParamCate()[0])) {
                                if (!stack.isEmpty()) {
                                    throw new RuntimeException("word模板参数解析失败");
                                }
                                //标注当前找到的占位符
                                paramCate = value;
                                isStack = true;
                            }
                        }
                    }

                    //如果找到了占位符开始标志,后续的字符需要入栈
                    if (isStack) {
                        stack.push(text.charAt(i));
                        //已经遍历到当前占位符的尾部,表示占位符的所有字符都已入栈,需要从栈中获取参数
                        if (paramCate.getParamCate()[1].charAt(0) == text.charAt(i)) {
                            isStack = false;
                            isAnalysis = false;
                            params.add(getParam(stack, paramCate, path));
                        }
                    }
                }
            }
        }
        document.setParams(params);
    }

    private TemplateParam getParam(Stack stack, ParamCategory paramCategory, String path) {
        int size = stack.size();
        StringBuilder tempParam = new StringBuilder();
        for (int i = 0; i < size; i++) {
            tempParam.append(stack.pop());
        }
        tempParam = tempParam.reverse();
        int startIndex = tempParam.indexOf(paramCategory.getParamCate()[0]) + 2;
        int endIndex = tempParam.indexOf(paramCategory.getParamCate()[1]);
        String param = tempParam.substring(startIndex, endIndex);
        boolean matches = Pattern.matches(RULE, param);
        if (!matches) {
            throw new RuntimeException("word模板参数格式错误");
        }
        TemplateParam templateParam = new TemplateParam();
        switch (paramCategory.getParamType()) {
            case LOOP:
                templateParam = TemplateParam.buildCollParam(param, path);
                break;
            case IMAGE:
                templateParam = TemplateParam.buildImageParam(param,path);
                break;
            case SINGLE:
                templateParam = TemplateParam.buildSingleParam(param, path);
                break;
            case CHECKBOX:
                break;
            default:
                templateParam = null;
                break;
        }
        return templateParam;
    }
}
import org.dom4j.Document;
import org.dom4j.Element;

import java.util.List;

/**
 * 对模板文档的封装,会在文档加载的时候初始化,并用于处理的各个环节
 * Created by yangxiangjun on 2021/1/26.
 */
public class DocumentModel {
    /**
     * xml文档对象
     */
    private Document document;
    /**
     * 源文件路径
     */
    private String filePath;
    /**
     * 生成ftl模板的路径
     */
    private String ftlPath;
    /**
     * 文档中的参数集合
     */
    protected List<TemplateParam> params;
    /**
     * xml文档中需要处理的节点
     */
    protected List<Element> nodes;
    ...
}
import com.tss.mangosercivea.manager.template.enums.ParamType;

/**
 * 模板中的参数
 * Created by yangxiangjun on 2021/1/8.
 */
public class TemplateParam {
    /**
     * 参数名
     */
    private String name;
    /**
     * 参数的前缀,代表列表参数属于哪个对象
     */
    private String ascription;
    /**
     * 参数出现在xml文档的路径
     */
    private String path;
    /**
     * 参数类型
     */
    private ParamType paramType;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getAscription() {
        return ascription;
    }

    public void setAscription(String ascription) {
        this.ascription = ascription;
    }

    public String getPath() {
        return path;
    }

    public void setPath(String path) {
        this.path = path;
    }

    public ParamType getParamType() {
        return paramType;
    }

    public void setParamType(ParamType paramType) {
        this.paramType = paramType;
    }

    public static TemplateParam buildParam(String text, String path){
        TemplateParam templateParam = new TemplateParam();
        templateParam.setAscription("");
        templateParam.setName(text);
        templateParam.setPath(path);
        return templateParam;
    }

    public static TemplateParam buildSingleParam(String text, String path){
        TemplateParam templateParam = buildParam(text, path);
        templateParam.setParamType(ParamType.SINGLE);
        return templateParam;
    }

    public static TemplateParam buildImageParam(String text, String path){
        TemplateParam templateParam = buildParam(text, path);
        templateParam.setParamType(ParamType.IMAGE);
        return templateParam;
    }

    public static TemplateParam buildCollParam(String text,String path){
        TemplateParam templateParam = new TemplateParam();
        String[] split = text.split("\\.");
        if (split.length != 2) {
            throw new RuntimeException("word模板循环参数格式错误");
        }
        templateParam.setAscription(split[0]);
        templateParam.setName(split[1]);
        templateParam.setParamType(ParamType.LOOP);
        templateParam.setPath(path);
        return templateParam;
    }

    @Override
    public String toString() {
        return "TemplateParam{" +
                "name='" + name + '\'' +
                ", ascription='" + ascription + '\'' +
                ", path='" + path + '\'' +
                ", paramType=" + paramType +
                '}';
    }
}
/**
 * 对模板中参数占位符的定义
 */
public enum ParamCategory {
    /**
     * 普通单次使用的参数占位符
     */
    SINGLE(ParamType.SINGLE, new String[]{"${", "}"}),
    /**
     * 列表参数占位符
     */
    LOOP(ParamType.LOOP, new String[]{"$[", "]"}),
    /**
     * 图片参数占位符
     */
    IMAGE(ParamType.IMAGE, new String[]{"@{", "}"}),
    /**
     * 复选框参数占位符
     */
    CHECKBOX(ParamType.CHECKBOX, new String[]{"#{", "}"}),
    ;

    private ParamType paramType;
    private String[] paramCate;

    ParamCategory(ParamType paramType, String[] paramCate) {
        this.paramType = paramType;
        this.paramCate = paramCate;
    }

    public ParamType getParamType() {
        return paramType;
    }

    public void setParamType(ParamType paramType) {
        this.paramType = paramType;
    }

    public String[] getParamCate() {
        return paramCate;
    }

    public void setParamCate(String[] paramCate) {
        this.paramCate = paramCate;
    }
}

2、实现对参数的校正,word格式转换为xml的过程很可能会导致参数在xml中被打乱,所以我们解析出来参数之后,需要去校验参数是否符合预期,被打乱的参数需要校正,除此之外,在配置模板时,普通参数、列表参数、图片参数等的占位符是做了区别的,分别是${} $[]和@{},这里会替换成freemaker规范的占位符

/**
 * 对参数进行占位符的检测,如果占位符和参数被分开,不在同一个标签下,进行校正
 *
 * Created by yangxiangjun on 2021/1/22.
 */
public class NormalizationHandler extends XMLHandler {

    @Override
    public void run(DocumentModel document) {
        List<Element> nodes = document.getNodes();
        List<TemplateParam> params = document.getParams();
        //获取所有的文本信息节点
        for (Element node : nodes) {
            //获取节点的文本信息
            String text = node.getText();
            //如果文本信息包好占位符,但是占位符不全,则首先将占位符去除
            for (ParamCategory value : ParamCategory.values()) {
                if (text.contains(value.getParamCate()[0]) ^ text.contains(value.getParamCate()[1])) {
                    text.replace(value.getParamCate()[0],"");
                    text.replace(value.getParamCate()[1],"");
                    node.setText(text);
                }
            }

            //如果文本信息中包含参数,判断参数是否有正确的占位符,没有正确的占位符,则添加正确的占位符
            String finalText = text;
            params.forEach((templateParam) -> {
                String param = templateParam.getParamType().equals(ParamType.LOOP) ? templateParam.getAscription() + "." + templateParam.getName() : templateParam.getName();
                String relParam = "";
                String paramCategory[] = new String[2];
                switch (templateParam.getParamType()){
                    case LOOP:
                        paramCategory = ParamCategory.LOOP.getParamCate();
                        break;
                    case IMAGE:
                        paramCategory = ParamCategory.IMAGE.getParamCate();
                        break;
                    case SINGLE:
                        paramCategory = ParamCategory.SINGLE.getParamCate();
                        break;
                    case CHECKBOX:
                        paramCategory = ParamCategory.CHECKBOX.getParamCate();
                        break;
                    default:
                        break;
                }
                relParam = paramCategory.length != 2 ? param : paramCategory[0] + param + paramCategory[1];
                node.setText(finalText.replace(param,relParam));
            });
        }
    }

}

3、列表参数处理,我们想要输出列表数据,就要在需要循环的数据外层加上list标签,这样freemaker在生成文档的时候才能循环数据

import com.tss.mangosercivea.manager.template.*;
import com.tss.mangosercivea.manager.template.enums.ParamCategory;
import com.tss.mangosercivea.manager.template.enums.ParamType;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;

import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;

/**
 * 实现word模板中列表参数的处理
 * Created by yangxiangjun on 2021/1/25.
 */
public class WordCollectionHandler extends XMLHandler {
    @Override
    public void run(DocumentModel document) {
        List<TemplateParam> params = document.getParams();
        //首先对参数进行筛选和分类,同一个集合的参数放到同一个列表里,剔除单参数
        Map<String, List<TemplateParam>> collParams = params.stream()
                .filter(param -> ParamType.LOOP.equals(param.getParamType()))
                .collect(Collectors.groupingBy(TemplateParam::getAscription));
        for (String asc : collParams.keySet()) {
            List<TemplateParam> templateParams = collParams.get(asc);
            AtomicReference<Element> ancestorTr = new AtomicReference<>();
            templateParams.forEach(templateParam -> {
                //根据参数所在标签的路径获取标签
                List<Element> nodes = document.getDocument().selectNodes(templateParam.getPath());
                nodes.get(0).setText(ParamCategory.SINGLE.getParamCate()[0] + templateParam.getAscription() + "." + templateParam.getName() + ParamCategory.SINGLE.getParamCate()[1]);
                //获取当前w:t的w:tr父节点
                List<Element> ancestorTrList = nodes.get(0).selectNodes("ancestor::w:tr[1]");
                if (!ancestorTrList.isEmpty()) {
                    Element element = ancestorTrList.get(0);
                    if (null != ancestorTr.get() && !element.equals(ancestorTr.get())) {
                        throw new RuntimeException("word文档关于循环数据的配置不符合规定");
                    }
                    ancestorTr.set(element);
                } else {
                    throw new RuntimeException("处理集合数据时,模板的配置只能借助表格");
                }
            });

            Element parent = ancestorTr.get().getParent();
            List<Element> elements = parent.elements();
            //获取w:tr标签的位置下标,用于添加循环标签后对其进行重写
            int index = elements.indexOf(ancestorTr.get());
            //创建一个#list标签,并设置循环数据的属性,这里的属性添加了name属性名,但实际上是不需要他的
            //这里只是为了能回写xml文件,后续会做处理
            Element foreach = DocumentHelper.createElement("#list");
            foreach.addAttribute("name", asc +" as "+asc);
            Element copy = ancestorTr.get().createCopy();
            foreach.add(copy);

            //添加if标签用于对循环数据的空判断
            Element iflist = DocumentHelper.createElement("#if");
            iflist.addAttribute("name",String.format("%s ??&& (%s?size>0)",asc,asc));
            iflist.add(foreach);
            elements.set(index,iflist);
        }
    }
}

4、图片参数处理,图片的配置方式是在模板中使用一张占位的图片,生成文档会使用新的图片数据将其替换,对于普通的图片,可以换行加上@{}占位符参数,但是有其他展示效果,如悬浮的,可能占位符无法生效,不见占位符的会用img1,2,3代替,图片处理目前验证不够充分,可能难以实现复杂场景

import cn.hutool.core.collection.CollectionUtil;
import com.tss.mangosercivea.manager.template.DocumentModel;
import com.tss.mangosercivea.manager.template.XMLHandler;
import com.tss.mangosercivea.util.XMLUtil;
import org.apache.commons.lang3.StringUtils;
import org.dom4j.Element;

import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * word的图片参数处理
 * Created by yangxiangjun on 2021/2/4.
 */
public class WordImageHandler extends XMLHandler {
    private final static String REGEX = "\\@\\{(\\w+)\\}";
    @Override
    public void run(DocumentModel document) {
        //图片索引下表
        Integer index = 1;
        //获取根路径
        Element root = document.getDocument().getRootElement();
        //获取图片标签
        List<Element> imgTagList = root.selectNodes("//w:binData");
        for (Element element : imgTagList) {
            element.setText(String.format("${img%s!''}",index++));
            //获取当前图片所在的wp标签
            List<Element> wpList = element.selectNodes("ancestor::w:p");
            if (CollectionUtil.isEmpty(wpList)) {
                throw new RuntimeException("未知异常");
            }
            Element imgWpElement = wpList.get(0);
            while (imgWpElement != null) {
                try {
                    imgWpElement = XMLUtil.selectNextElement(imgWpElement);
                } catch (Exception de) {
                    break;
                }
                //获取对应图片字段
                List<Element> imgFiledList = imgWpElement.selectNodes("w:r/w:t");
                if (CollectionUtil.isEmpty(imgFiledList)) {
                    continue;
                }
                String imgFiled = getImgFiledTrimStr(imgFiledList);
                Pattern compile = Pattern.compile(REGEX);
                Matcher matcher = compile.matcher(imgFiled);
                String imgFiledStr = "";
                while (matcher.find()) {
                    imgFiledStr = matcher.group(1);
                    boolean remove = imgWpElement.getParent().elements().remove(imgWpElement);
                    System.out.println(remove);
                }
                if (StringUtils.isNotEmpty(imgFiledStr)) {
                    element.setText(String.format("${%s!''}",imgFiledStr));
                    break;
                }
            }

        }
    }

    private String getImgFiledTrimStr(List<Element> imgFiledList) {
        StringBuilder stringBuilder = new StringBuilder();
        if (CollectionUtil.isNotEmpty(imgFiledList)) {
            for (Element element : imgFiledList) {
                stringBuilder.append(element.getTextTrim().toString());
            }
        }
        return stringBuilder.toString();
    }

}

5、回写xml文档,将处理后的xml回写,注意回写xml文档是需要重写XMLWriter,避免在回写的过程中特殊字符被转换

import com.tss.mangosercivea.manager.template.core.ConvertXmlWriter;
import com.tss.mangosercivea.manager.template.DocumentModel;
import com.tss.mangosercivea.manager.template.XMLHandler;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.XMLWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;

/**
 * 将处理后的document对象重新写入文件
 * Created by yangxiangjun on 2021/1/27.
 */
public class XMLWriterHandler extends XMLHandler {

    private static Logger log = LoggerFactory.getLogger(XMLWriterHandler.class);

    @Override
    public void run(DocumentModel document) {
        OutputStreamWriter outputStreamWriter = null;
        XMLWriter writer = null;
        try {
            outputStreamWriter = new OutputStreamWriter(new FileOutputStream(document.getFilePath()));
            OutputFormat format = OutputFormat.createPrettyPrint();
            format.setEncoding("UTF-8");    // 指定XML编码
            writer = new ConvertXmlWriter(outputStreamWriter,format);
            writer.write(document.getDocument());
        } catch (FileNotFoundException e) {
            log.error("找不到指定文件",e);
        } catch (IOException e) {
            log.error("重新写xml文件失败",e);
        } finally {
            try {
                if (outputStreamWriter != null) {
                    outputStreamWriter.close();
                }
                if (writer!= null) {
                    writer.close();
                }
            } catch (IOException e) {

            }
        }
    }
}

import org.dom4j.io.OutputFormat;
import org.dom4j.io.XMLWriter;

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;

/**
 * 重写writer方法,对xml文件中新添加的特殊字符进行转义
 *
 * Created by yangxiangjun on 2021/1/27.
 */
public class ConvertXmlWriter extends XMLWriter {

    public ConvertXmlWriter(OutputStreamWriter out, OutputFormat format) throws UnsupportedEncodingException {
        super(out, format);
    }

    @Override
    protected void writeEscapeAttributeEntities(String txt) throws IOException {
        if (txt != null) {
            this.writer.write(txt);
        }
    }
}

6、将xml文档转换为ftl模板,并且在这个转换过程中需要去除list标签的name属性的key,只留下value

import com.tss.mangosercivea.manager.template.DocumentModel;
import com.tss.mangosercivea.manager.template.XMLHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

import java.io.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static java.util.regex.Pattern.compile;

/**
 * 将xml文档转换为ftl模板
 * Created by yangxiangjun on 2021/2/4.
 */
@Slf4j
public class XmlToFtlHandler extends XMLHandler {

    @Override
    public void run(DocumentModel document) {
        this.replacePartTextContent(document);
    }
    
    /**
    * @Description 目前需要修改的集合属性,因为xml不支持ftl的list标签。这里需要我们修改
    * @Param
    * @return void
    */
    public synchronized void replacePartTextContent(DocumentModel documentModel) {
        String filePath = documentModel.getFilePath();
        // 读
        try {
            File file = new File(filePath);
            File targetFile = new File(filePath.substring(0,filePath.lastIndexOf("."))+".ftl");
            BufferedReader bufIn = new BufferedReader(new InputStreamReader(new FileInputStream(file),"UTF-8"));
            // 内存流, 作为临时流
            CharArrayWriter tempStream = new CharArrayWriter();
            // 替换
            String line = null;
            while ((line = bufIn.readLine()) != null) {
                if (StringUtils.isBlank(line)) {
                    continue;
                }
                // 替换每行中, 符合条件的字符串
                line = replacePart(line);
                // 将该行写入内存
                tempStream.write(line);
                // 添加换行符
                //tempStream.append(System.getProperty("line.separator"));
            }
            // 关闭 输入流
            bufIn.close();
            // 将内存中的流 写入 文件
            BufferedWriter out = new BufferedWriter(
                    new OutputStreamWriter(
                            new FileOutputStream(targetFile), "UTF-8"));
            tempStream.writeTo(out);
            out.close();
            documentModel.setFtlPath(targetFile.getPath());
        } catch (Exception e) {
            log.error("xml转ftl失败",e);
        }

    }

    public String replacePart(String line) {
        StringBuffer sb = new StringBuffer();
        Pattern p = compile("(\\#(\\w+) name=\"([ \\>\\?\\&\\!\\'\\$\\(\\)\\{\\}\\,\\.\\=\\f\\n\\r\\t\\vA-Za-z0-9_]+)\")");
        Matcher m = p.matcher(line) ;
        while( m.find() ){
            String labelName = m.group(2);
            String group = m.group(3);
            String v = "#"+labelName+" "+group;
            //注意,在替换字符串中使用反斜线 (\) 和美元符号 ($) 可能导致与作为字面值替换字符串时所产生的结果不同。
            //美元符号可视为到如上所述已捕获子序列的引用,反斜线可用于转义替换字符串中的字面值字符。
            v = v.replace("\\", "\\\\").replace("$", "\\$");
            //替换掉查找到的字符串
            m.appendReplacement(sb, v) ;
        }
        //别忘了加上最后一点
        m.appendTail(sb) ;
        return sb.toString();
    }
}

到这里就完成了几个基本的处理过程,生成了ftl模板文件,可以用于freemaker生成文档了,下面要做的,就是利用责任链模式,将上面这些步骤依次执行。

这是比较核心的一个类,他用于构建责任链模式,并且控制从docx格式的word文档模板到生成word文档结果的全过程。

import com.tss.mangosercivea.manager.template.DocumentModel;
import com.tss.mangosercivea.manager.template.XMLHandler;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import org.apache.commons.lang3.StringUtils;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.io.SAXReader;

import java.io.*;
import java.util.Map;

/**
 * Created by yangxiangjun on 2021/1/26.
 */
public class DocExecutor {
    public static final String LABEL_TEXT = "//w:t";
    private Map<String, Object> dataMap;
    private String sourceFilePath;
    private String targetFilePath;
    private DocumentModel documentModel;
    private XMLHandler header;
    private XMLHandler tail;

    public DocExecutor(Map<String, Object> dataMap, String sourceFilePath, String targetFilePath) {
        this.dataMap = dataMap;
        this.sourceFilePath = sourceFilePath;
        this.targetFilePath = targetFilePath;
    }

    public DocExecutor addChain(XMLHandler chain) {
        if (header == null) {
            this.header = this.tail = chain;
            return this;
        }
        this.tail.next(chain);
        this.tail = chain;
        return this;
    }

    public File build() {
        try {
            init();
        } catch (DocumentException e) {
            throw new RuntimeException("文档初始化失败", e);
        }
        this.header.execute(documentModel);
        File generateFile;
        try {
            generateFile = generate();
        } catch (IOException e) {
            throw new RuntimeException("文档生成失败", e);
        }
        destroy();
        return generateFile;
    }

    /**
     * 初始化document对象
     *
     * @throws DocumentException
     */
    private void init() throws DocumentException {
        if (StringUtils.isBlank(sourceFilePath)) {
            throw new RuntimeException("源模板文件为空");
        }
        convertToXml();
        SAXReader reader = new SAXReader();
        File file = new File(sourceFilePath);
        Document document = reader.read(file);
        DocumentModel documentModel = new DocumentModel();
        documentModel.setDocument(document);
        documentModel.setFilePath(sourceFilePath);
        documentModel.setNodes(document.selectNodes(LABEL_TEXT));
        this.documentModel = documentModel;
    }

    private void convertToXml() {
        File file = new File(sourceFilePath);
        if (file.isFile()) {
            String name = sourceFilePath.substring(0, sourceFilePath.lastIndexOf(".")) + ".xml";
            file.renameTo(new File(name));
            this.sourceFilePath = name;
        } else {
            throw new RuntimeException("找不到源文件");
        }
    }

    private File generate() throws IOException {
        if (StringUtils.isBlank(documentModel.getFtlPath())) {
            throw new RuntimeException("模板文件为空");
        }
        if (StringUtils.isBlank(targetFilePath)) {
            throw new RuntimeException("目标文件为空");
        }

        File file = new File(documentModel.getFtlPath());
        String directoryPath;
        String fileName;
        if (file.isFile()) {
            directoryPath = file.getParent();
            fileName = file.getName();
        } else {
            throw new RuntimeException("找不到ftl模板文件");
        }

        Configuration configuration = FreeMarkerConfig.getInstance();
        // 设置FreeMarker生成文档所需要的模板的路径
        try {
            configuration.setDirectoryForTemplateLoading(new File(directoryPath));
        } catch (IOException e) {
            throw new RuntimeException("找不到ftl模板文件所在的路径");
        }
        // 设置FreeMarker生成Word文档所需要的模板
        Template t = configuration.getTemplate(fileName);
        // 创建一个Word文档的输出流
        File targetFile = new File(targetFilePath);
        try (Writer out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(targetFile), "UTF-8"));) {
            //FreeMarker使用Word模板和数据生成Word文档
            t.process(dataMap, out);
            return file;
        } catch (TemplateException e) {
            throw new RuntimeException("文档生成失败", e);
        }
    }

    private void destroy() {
        //删除处理过程中产生的文件
//        deleteFile(DIR_PATH + sourceFileName);
        deleteFile(documentModel.getFtlPath());
        //处理生成的最终文档,上传文件服务器或构建流进行传输
    }

    /**
     * 删除单个文件
     *
     * @param fileName 要删除的文件的文件名
     * @return 单个文件删除成功返回true,否则返回false
     */
    private static boolean deleteFile(String fileName) {
        File file = new File(fileName);
        // 如果文件路径所对应的文件存在,并且是一个文件,则直接删除
        if (file.exists() && file.isFile()) {
            if (file.delete()) {
                return true;
            } else {
                return false;
            }
        } else {
            return false;
        }
    }
}
/**
 * freemaker导出excel的公共配置类
 * Created by yangxiangjun on 2021/1/27.
 */
public class FreeMarkerConfig {
    private static Logger log = LoggerFactory.getLogger(FreeMarkerConfig.class);

    public static Configuration getInstance(){
        return ConfigEnum.CONFIG_INSTANCE.getInstance();
    }

    public enum ConfigEnum{
        /**
         *
         */
        CONFIG_INSTANCE;

        private Configuration instance;

        ConfigEnum() {
            // 设置FreeMarker的版本和编码格式
            Configuration configuration = new Configuration(new Version("2.3.28"));
            configuration.setDefaultEncoding("UTF-8");
            this.instance = configuration;
        }

        public Configuration getInstance() {
            return instance;
        }
    }
}
/**
 * 文档生成器,可继承对word、excel、pdf等各种文档的实现
 * Created by yangxiangjun on 2021/1/29.
 */
public interface DocumentGenerator {
    DocumentType getType();

    File generateFile(Map<String, Object> dataMap, String sourceFilePath, String targetFilePath);
}
/**
 * word文档生成的实现
 * Created by yangxiangjun on 2021/2/3.
 */
@Component
public class WordDocument implements DocumentGenerator {
    @Override
    public DocumentType getType() {
        return DocumentType.WORD;
    }

    @Override
    public File generateFile(Map<String, Object> dataMap, String sourceFilePath, String targetFilePath) {
        DocExecutor builder = new DocExecutor(dataMap,sourceFilePath,targetFilePath);
        builder.addChain(new AnalysisParamHandler())
                .addChain(new NormalizationHandler())
                .addChain(new WordCollectionHandler())
                .addChain(new WordImageHandler())
                .addChain(new XMLWriterHandler())
                .addChain(new XmlToFtlHandler());
        File build = builder.build();
        return build;
    }
}
import com.tss.mangosercivea.manager.template.enums.DocumentType;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;

/**
 * 文档生成器的工厂,这里会根据文件类型的不同,产生不同的处理器
 * Created by yangxiangjun on 2021/2/3.
 */
@Component
public class DocumentFactory implements ApplicationContextAware {
    private List<DocumentGenerator> generators;

    /**
     * 根据类型从DocumentGenerator的实现类中选取对应的处理器
     * @param documentType
     * @return
     */
    public DocumentGenerator createGenerator(DocumentType documentType){
        Optional<DocumentGenerator> generatorOptional = generators.stream().filter(generator -> documentType.equals(generator.getType())).findFirst();
        if (generatorOptional.isPresent()) {
            return generatorOptional.get();
        } else {
            throw new RuntimeException("找不到对应的文档处理器");
        }
    }

    /**
     * 系统初始化时将所有DocumentGenerator的实现类加载
     * @param applicationContext
     * @throws BeansException
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        generators = new ArrayList<>();
        Map<String, DocumentGenerator> beansOfType = applicationContext.getBeansOfType(DocumentGenerator.class);
        generators.addAll(beansOfType.values());
    }
}

借助设计模式的设计,我们在调用功能的时候,可以变得简单明了,传入数据和文件即可。

/**
 * Created by yangxiangjun on 2021/2/3.
 */
@SpringBootTest
public class WordExportTest {

    @Autowired
    DocumentFactory documentFactory;

    @Test
    public void test(){
        Map<String, Object> dataMap = new HashMap<>();
        dataMap.put("title","测试");

        Map<String,Object> map = new HashMap<>();
        List<Map<String,Object>> list = new ArrayList<>();
        map.put("name", "用户1");
        map.put("sex", "男");
        map.put("age", 20);
        map.put("birth","2018-12-12");
        list.add(map);

        Map<String,Object> map1 = new HashMap<>();
        map1.put("name", "用户2");
        map1.put("sex", "女");
        map1.put("age", 18);
        map1.put("birth","2020-12-12");
        list.add(map1);

        dataMap.put("user",list);

        dataMap.put("image","图片文件base64");
        DocumentGenerator generator = documentFactory.createGenerator(DocumentType.WORD);
        generator.generateFile(dataMap,"C:\\Users\\mango\\Documents\\项目资料\\demo.docx","C:\\Users\\mango\\Documents\\项目资料\\123.docx");
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值