1. 程式人生 > >Java:封裝POI實現word的docx檔案的簡單模板功能

Java:封裝POI實現word的docx檔案的簡單模板功能

一:場景
通過Word模板來實現動態的word生成

二: 基本要求
1:替換文字中的內容
2:替換表格中的內容(不用動態生成表格)
3:替換後的內容應該與替換前的內容格式相同
4:模板修改方便
5:效果如下:
模板:
這裡寫圖片描述
結果:
這裡寫圖片描述
三:poi分析
使用方法:直接讀取word檔案,替換裡面各個部分的內容
優點:直接使用word檔案作為模板
缺點:本身的替換邏輯無法保留格式

四:為什麼選擇封裝POI
1:因為時間和學習成本(懶)的問題,沒有研究docx的xml規則,因此決定直接對現有的工具進行封裝,來實現需求.

2:freeMarker本身只是對通用的模板進行處理,底層並不能直接解析word檔案.
而poi本身就是對word檔案進行操作的,因此可以對直接在poi的api上進一步的封裝.

五:可行性分析
1.POI使用XWPFDocument物件來解析docx的檔案,通過在構造時傳入docx檔案的讀取流完成解析過程,將解析後將資訊分門別類的儲存在不同的物件中.並提供write(OutputStream stream)方法將這些物件重新轉換為xml寫入到檔案中.

2.XWPFDocument中的物件儲存有文字和格式等資訊,能夠拿到或修改這些資訊

3.結合上述1,2兩點,表示了POI存在修改word文字並保留格式的可能,通過編寫Demo修改了XWPFDocument中的一個XWPFParagraph物件中的XWPFRun文字內容後證明了這一點.

XWPFDocument docx = new XWPFDocument(InputStream);
XWPFRun run = docx.getParagraphs().get(0).getRuns().get(0); run.setText("modify"); docx.write(OutputStream);

六: 實現原理
1.文字內容和表格內容分別可以通過 getParagraphs() 和 getTables() 兩個方法得到

XWPFDocument docx = new XWPFDocument(InputStream);
//文字內容
List<XWPFParagraph> allXWPFParagraphs = docx.getParagraphs();
//表格內容
List<XWPFTable> xwpfTables = docx.getTables();

2.檢視XWPFTable發現,最後表格中的文字內容還是用XWPFParagraph物件儲存

//獲得每行
    List<XWPFTableRow> xwpfTableRows = = xwpfTable.getRows();
    List<XWPFTableCell> xwpfTableCells = new ArrayList<XWPFTableCell>();
    for(int i = 0 ; i < xwpfTableRows.size() ; i++){
            //獲得一行的所有單元
        xwpfTableCells.addAll(getCells(i));
    }
    List<XWPFParagraph> xwpfParagraphs = new ArrayList<XWPFParagraph>();
    for(XWPFTableCell cell : xwpfTableCells){
            //獲得每個單元中的文字
        xwpfParagraphs.addAll(cell.getParagraphs());
    }

因此很可能最後修改表格和文字最後都彙集到一個相同的地方,就是修改XWPFParagraph的文字內容.進過簡單的測試,證明了無論是表格還是文字,其內容都可以通過XWPFParagraph進行修改.

3.研究XWPFParagraph中的內容發現,每個XWPFParagraph物件中儲存的都是一整段的內容,而這一整段的內容被分割成多個部分存入了XWPFParagraph物件中的集合List runs,也許因為docx檔案轉換成xml的時候很大機率的會將相同格式的文字分割開來,所以每個XWPFRun展示的內容都是不連續的!!而這正是實現word模板的難點所在.

4.因為沒打算再深入研究docx轉xml的原理,因此打算在現有的不連續的基礎上進行封裝,將其組合起來,模擬成”連續”的物件,最後對”連續”的物件進行replace操作.我的實現方法如下
(1)獲得完整的文字內容,用於判斷是否包含${key}
(2)對文字內容的每個字元標記所屬的XWPFRun物件
(3)如果匹配,則從標記中獲取匹配的第一個字元,得到字元對應的標記物件,將替換的內容全部標記為該物件
(4)替換完成後,遍歷所有的標記,將標記物件所屬的字元重新組合成String後重新設定,並將無用的標記物件文字設定為空

public class XWPFParagraphUtils {

    private XWPFParagraph paragraph;

    private List<XWPFRun> allXWPFRuns;
    //所有run的String合併後的內容
    private StringBuffer context ;
    //長度與context對應的RunChar集合
    List<RunChar> runChars ;

    public XWPFParagraphUtils(XWPFParagraph paragraph){
        this.paragraph = paragraph;
        initParameter();
    }

    /**
     * 初始化各引數
     */
    private void initParameter(){
        context = new StringBuffer();
        runChars = new ArrayList<XWPFParagraphUtils.RunChar>();
        allXWPFRuns = new ArrayList<XWPFRun>();
        setXWPFRun();
    }


    /**
     * 設定XWPFRun相關的引數
     * @param run
     * @throws Exception
     */
    private void setXWPFRun() {

        allXWPFRuns = paragraph.getRuns();
        if(allXWPFRuns == null || allXWPFRuns.size() == 0){
            return;
        }else{
            for (XWPFRun run : allXWPFRuns) {
                int testPosition = run.getTextPosition();
                String text = run.getText(testPosition);
                if(text == null || text.length() == 0){
                    return;
                }

                this.context.append(text);
                for(int i = 0 ; i < text.length() ; i++){
                    runChars.add(new RunChar(text.charAt(i), run));
                }
            }
        }
        System.out.println(context.toString());
    }

    public String getString(){
        return context.toString();
    }

    public boolean contains(String key){
        return context.indexOf(key) >= 0 ? true : false;
    }

    /**
     * 所有匹配的值替換為對應的值
     * @param key(匹配模板中的${key})
     * @param value 替換後的值
     * @return
     */
    public boolean replaceAll(String key,String value){
        boolean replaceSuccess = false;
        key = "${" + key + "}";
        while(replace(key, value)){
            replaceSuccess = true;
        }
        return replaceSuccess;
    }

    /**
     * 所有匹配的值替換為對應的值(key匹配模板中的${key})
     * @param param 要替換的key-value集合
     * @return
     */
    public boolean replaceAll(Map<String,String> param){
        Set<Entry<String, String>> entrys = param.entrySet();
        boolean replaceSuccess = false;
        for (Entry<String, String> entry : entrys) {
            String key = entry.getKey();
            boolean currSuccessReplace = replaceAll(key,entry.getValue());
            replaceSuccess = replaceSuccess?replaceSuccess:currSuccessReplace;
        }
        return replaceSuccess;
    }

    /**
     * 將第一個匹配到的值替換為對應的值
     * @param key 
     * @param value
     * @return
     */
    private boolean replace(String key,String value){
        if(contains(key)){
            /*
             * 1:得帶key對應的開始和結束下標
             */
            int startIndex = context.indexOf(key);
            int endIndex = startIndex+key.length();
            /*
             * 2:獲取第一個匹配的XWPFRun
             */
            RunChar startRunChar = runChars.get(startIndex);
            XWPFRun startRun = startRunChar.getRun();
            /*
             * 3:將匹配的key清空
             */
            runChars.subList(startIndex, endIndex).clear();
            /*
             * 4:將value設定到startRun中
             */
            List<RunChar> addRunChar = new ArrayList<XWPFParagraphUtils.RunChar>();
            for(int i = 0 ; i < value.length() ; i++){
                addRunChar.add(new RunChar(value.charAt(i), startRun));
            }
            runChars.addAll(startIndex, addRunChar);
            resetRunContext(runChars);
            return true;
        }else{
            return false;
        }
    }

    private void resetRunContext(List<RunChar> newRunChars){
        /**
         * 生成新的XWPFRun與Context的對應關係
         */
        HashMap<XWPFRun, StringBuffer> newRunContext = new HashMap<XWPFRun, StringBuffer>();
        //重設context
        context = new StringBuffer();
        for(RunChar runChar : newRunChars){
            StringBuffer newRunText ;
            if(newRunContext.containsKey(runChar.getRun())){
                newRunText = newRunContext.get(runChar.getRun());
            }else{
                newRunText = new StringBuffer();
            }
            context.append(runChar.getValue());
            newRunText.append(runChar.getValue());
            newRunContext.put(runChar.getRun(), newRunText);
        }

        /**
         * 遍歷舊的runContext,替換context
         * 並重新設定run的text,如果不匹配,text設定為""
         */
        for(XWPFRun run : allXWPFRuns){
            if(newRunContext.containsKey(run)){
                String newContext = newRunContext.get(run).toString();
                XWPFRunUtils.setText(run,newContext);
            }else{
                XWPFRunUtils.setText(run,"");
            }
        }
    }

    /**
     * 實體類:儲存位元組與XWPFRun物件的對應關係
     * @author JianQiu
     */
    class RunChar{
        /**
         * 位元組
         */
        private char value;
        /**
         * 對應的XWPFRun
         */
        private XWPFRun run;
        public RunChar(char value,XWPFRun run){
            this.setValue(value);
            this.setRun(run);
        }
        public char getValue() {
            return value;
        }
        public void setValue(char value) {
            this.value = value;
        }
        public XWPFRun getRun() {
            return run;
        }
        public void setRun(XWPFRun run) {
            this.run = run;
        }

    }
}

5.最後只需呼叫XWPFDocument.write(OutputStream)方法即可得到基於模板生成的docx檔案了.

七:小結
完整的程式碼我已上傳到GitHub,裡面包含了說明以及測試用例,可以作為參考.
https://github.com/DeveloperHuang/WordHandler
目前只是簡單的實現了替換的功能,後續功能可能下半年才有空研究了.