完美解決docx4j變量替換問題

 最近工作上需要自己完成word文檔變量替換的問題

把裏面的變量給替換成數據庫裏的值,但是由於在word文檔渲染成xml的時候,會通過某些原因把字段放在不同層次的xml標籤

上面是docx4j文檔說的原因,大概是字體格式不同(我的問題是用了粗體 ${ 和 正常中文是不同格式的),拼寫語法問題,編輯順序。

在StackOverflow 找了很久解決方案,Variableprepare.prepare方法確實測試後能解決部分替換問題,但還是不能滿足我的需求。

奈何自己水平不夠,對word這部份不熟悉,docx4j的源碼質量也不是很高,沒辦法很快理解,最後還是一個比較厲害的同事幫我解決的。

docx4j版本

 <!--docx4j支持 start-->
        <dependency>
            <groupId>org.docx4j</groupId>
            <artifactId>docx4j</artifactId>
            <version>3.3.6</version>
            <exclusions>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-log4j12</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.docx4j</groupId>
            <artifactId>docx4j-export-fo</artifactId>
            <version>3.3.6</version>
        </dependency>
        <!--docx4j支持 end-->

下面是工具類

package zwy.saas.common.util;


import org.docx4j.XmlUtils;
import org.docx4j.jaxb.Context;
import org.docx4j.openpackaging.packages.WordprocessingMLPackage;
import org.docx4j.openpackaging.parts.WordprocessingML.MainDocumentPart;
import org.docx4j.wml.Document;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.HttpServletResponse;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.Map;
import java.util.regex.Pattern;

/**
 * 關於文件操作的工具類
 *
 * @author kaizen
 * @date 2018-10-23 17:21:36
 */
public final class Docx4jUtils {

    private static final Logger logger = LoggerFactory.getLogger(Docx4jUtils.class);

    /**
     * 替換變量並下載word文檔
     *
     * @param inputStream
     * @param map
     * @param response
     * @param fileName
     */
    public static void downloadDocUseDoc4j(InputStream inputStream, Map<String, String> map,
                                          HttpServletResponse response, String fileName) {

        try {
            // 設置響應頭
            fileName = URLEncoder.encode(fileName, "UTF-8");
            response.setContentType("application/octet-stream;charset=UTF-8");
            response.setCharacterEncoding("utf-8");
            response.setHeader("Content-Disposition", "attachment; filename=" + fileName + ".docx");
            response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");

            OutputStream outs  = response.getOutputStream();
            Docx4jUtils.replaceDocUseDoc4j(inputStream,map,outs);

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

    /**
     * 替換變量並輸出word文檔
     * @param inputStream
     * @param map
     * @param outputStream
     */
    public static void replaceDocUseDoc4j(InputStream inputStream, Map<String, String> map,
                                          OutputStream outputStream) {
        try {
            WordprocessingMLPackage doc = WordprocessingMLPackage.load(inputStream);
            MainDocumentPart mainDocumentPart = doc.getMainDocumentPart();
            if (null != map && !map.isEmpty()) {
                // 將${}裏的內容結構層次替換爲一層
                Docx4jUtils .cleanDocumentPart(mainDocumentPart);
                // 替換文本內容
                mainDocumentPart.variableReplace(map);
            }

            // 輸出word文件
            doc.save(outputStream);
            outputStream.flush();
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
    }


    /**
     * cleanDocumentPart
     *
     * @param documentPart
     */
    public static boolean cleanDocumentPart(MainDocumentPart documentPart) throws Exception {
        if (documentPart == null) {
            return false;
        }
        Document document = documentPart.getContents();
        String wmlTemplate =
                XmlUtils.marshaltoString(document, true, false, Context.jc);
        document = (Document) XmlUtils.unwrap(DocxVariableClearUtils.doCleanDocumentPart(wmlTemplate, Context.jc));
        documentPart.setContents(document);
        return true;
    }

    /**
     * 清掃 docx4j 模板變量字符,通常以${variable}形式
     * <p>
     * XXX: 主要在上傳模板時處理一下, 後續
     *
     * @author liliang
     * @since 2018-11-07
     */
    private static class DocxVariableClearUtils {


        /**
         * 去任意XML標籤
         */
        private static final Pattern XML_PATTERN = Pattern.compile("<[^>]*>");

        private DocxVariableClearUtils() {
        }

        /**
         * start符號
         */
        private static final char PREFIX = '$';

        /**
         * 中包含
         */
        private static final char LEFT_BRACE = '{';

        /**
         * 結尾
         */
        private static final char RIGHT_BRACE = '}';

        /**
         * 未開始
         */
        private static final int NONE_START = -1;

        /**
         * 未開始
         */
        private static final int NONE_START_INDEX = -1;

        /**
         * 開始
         */
        private static final int PREFIX_STATUS = 1;

        /**
         * 左括號
         */
        private static final int LEFT_BRACE_STATUS = 2;

        /**
         * 右括號
         */
        private static final int RIGHT_BRACE_STATUS = 3;


        /**
         * doCleanDocumentPart
         *
         * @param wmlTemplate
         * @param jc
         * @return
         * @throws JAXBException
         */
        private static Object doCleanDocumentPart(String wmlTemplate, JAXBContext jc) throws JAXBException {
            // 進入變量塊位置
            int curStatus = NONE_START;
            // 開始位置
            int keyStartIndex = NONE_START_INDEX;
            // 當前位置
            int curIndex = 0;
            char[] textCharacters = wmlTemplate.toCharArray();
            StringBuilder documentBuilder = new StringBuilder(textCharacters.length);
            documentBuilder.append(textCharacters);
            // 新文檔
            StringBuilder newDocumentBuilder = new StringBuilder(textCharacters.length);
            // 最後一次寫位置
            int lastWriteIndex = 0;
            for (char c : textCharacters) {
                switch (c) {
                    case PREFIX:
                        // TODO 不管其何狀態直接修改指針,這也意味着變量名稱裏面不能有PREFIX
                        keyStartIndex = curIndex;
                        curStatus = PREFIX_STATUS;
                        break;
                    case LEFT_BRACE:
                        if (curStatus == PREFIX_STATUS) {
                            curStatus = LEFT_BRACE_STATUS;
                        }
                        break;
                    case RIGHT_BRACE:
                        if (curStatus == LEFT_BRACE_STATUS) {
                            // 接上之前的字符
                            newDocumentBuilder.append(documentBuilder.substring(lastWriteIndex, keyStartIndex));
                            // 結束位置
                            int keyEndIndex = curIndex + 1;
                            // 替換
                            String rawKey = documentBuilder.substring(keyStartIndex, keyEndIndex);
                            // 幹掉多餘標籤
                            String mappingKey = XML_PATTERN.matcher(rawKey).replaceAll("");
                            if (!mappingKey.equals(rawKey)) {
                                char[] rawKeyChars = rawKey.toCharArray();
                                // 保留原格式
                                StringBuilder rawStringBuilder = new StringBuilder(rawKey.length());
                                // 去掉變量引用字符
                                for (char rawChar : rawKeyChars) {
                                    if (rawChar == PREFIX || rawChar == LEFT_BRACE || rawChar == RIGHT_BRACE) {
                                        continue;
                                    }
                                    rawStringBuilder.append(rawChar);
                                }
                                // FIXME 要求變量連在一起
                                String variable = mappingKey.substring(2, mappingKey.length() - 1);
                                int variableStart = rawStringBuilder.indexOf(variable);
                                if (variableStart > 0) {
                                    rawStringBuilder = rawStringBuilder.replace(variableStart, variableStart + variable.length(), mappingKey);
                                }
                                newDocumentBuilder.append(rawStringBuilder.toString());
                            } else {
                                newDocumentBuilder.append(mappingKey);
                            }
                            lastWriteIndex = keyEndIndex;

                            curStatus = NONE_START;
                            keyStartIndex = NONE_START_INDEX;
                        }
                    default:
                        break;
                }
                curIndex++;
            }
            // 餘部
            if (lastWriteIndex < documentBuilder.length()) {
                newDocumentBuilder.append(documentBuilder.substring(lastWriteIndex));
            }
            return XmlUtils.unmarshalString(newDocumentBuilder.toString(), jc);
        }

    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章