DOCX4J 从现有模板简化生成报告

问题描述 投票:0回答:1

我正在使用 Java 17+ 以及 docx4j。我需要阅读包含以下内容的 Word 文档模板 标题带有占位符 ${creationDate},文档正文中有一个表格,其中包含以下信息:

ID    | First Name   |  Last Name  |  Age   |  Gender  |
-----------------------------------------------------------
${id} | ${firstName} | ${lastName} | ${age} | ${gender} |

并生成一个新文档,该文档将用应用程序中的数据替换占位符。 在表中,我们可能有多个人,因此我们需要实现 docx4j 将一行替换为占位符,以表示我们可能拥有的多行数据。

这起初看起来很简单,但我预期花了更长的时间。我使用 docx4j 来实现这个问题,并使用 StackOverflow 来记录这个解决方案。

如果有人有更好的方法来做到这一点,我将奖励其作为已接受的答案。

java docx4j
1个回答
0
投票

首先添加 docx4j 所需的依赖项,如下所示:

    <dependency>
      <groupId>org.docx4j</groupId>
      <artifactId>docx4j-core</artifactId>
      <version>11.5.0</version>
    </dependency>
    <dependency>
      <groupId>org.docx4j</groupId>
      <artifactId>docx4j-JAXB-ReferenceImpl</artifactId>
      <version>11.5.0</version>
    </dependency>
    <dependency>
      <groupId>javax.xml.bind</groupId>
      <artifactId>jaxb-api</artifactId>
      <version>2.3.1</version>
    </dependency>

下面是生成测试数据的主类:

public class App {

    public static void main(String[] args) throws Exception {

        //prepare the data
        List<Map<String, String>> tableData = new ArrayList<>();

        // First record.
        Map<String, String> record1 = new HashMap<>();
        record1.put("${id}", "1");
        record1.put("${first-name}", "John");
        record1.put("${last-name}", "Doe");
        record1.put("${age}", "30");
        record1.put("${gender}", "Male");
        tableData.add(record1);

        // second record
        Map<String, String> record2 = new HashMap<>();
        record2.put("${id}", "2");
        record2.put("${first-name}", "Jane");
        record2.put("${last-name}", "Smith");
        record2.put("${age}", "28");
        record2.put("${gender}", "Female");
        tableData.add(record2);

        Map<String, Object> data = new HashMap<>();
        data.put("${creation-date}", "2021-09-01");
        data.put("${title}", "Sample Document");
        data.put("${table0}", tableData);

        // setup
        WordDocumentFormatter wordDocumentFormatter = new WordDocumentFormatter();
        wordDocumentFormatter.formatDocument(data);
    }


}

下面是用应用程序中的数据替换模板的类:

public class WordDocumentFormatter {

    List<PlaceholderReplacer> placeholderReplacers;

    public WordDocumentFormatter() {
        placeholderReplacers = new ArrayList<>();
        placeholderReplacers.add(new HeaderPlaceholderReplacer());
        placeholderReplacers.add(new BodyPlaceholderReplacer());
        placeholderReplacers.add(new TablePlaceholderReplacer());
    }

    public void formatDocument(Map<String, Object> data) {
        WordprocessingMLPackage wordMLPackage = null;
        try {
            wordMLPackage = WordprocessingMLPackage.load(new File(getTemplatePath()));
            for (PlaceholderReplacer placeholderReplacer : placeholderReplacers) {
                placeholderReplacer.replacePlaceholders(wordMLPackage, data);
            }
            wordMLPackage.save(new File(getOutputPath()));
        } catch (Docx4JException e) {
        }
    }


    public String getTemplatePath() {
        String templatePath = "C:\\Users\\User\\Desktop\\test.docx";
        return templatePath;
    }

    public String getOutputPath() {
        String outputPath = "C:\\Users\\User\\Desktop\\output.docx";
        return outputPath;
    }
}

我们需要替换文档标题、文档正文和文档表格中的占位符,我使用策略模式来实现每个功能,如下所示:

  • 我们定义合约
public interface PlaceholderReplacer {
    void replacePlaceholders(WordprocessingMLPackage wordMLPackage, Map<String, Object> data);

    default void replacePlaceholderInContent(Object content, Map<String, Object> data) {
        content = XmlUtils.unwrap(content);

        // If the content is a paragraph, iterate through its runs.
        if (content instanceof P) {
            P paragraph = (P) content;

            // Iterate through each run inside the paragraph.
            for (Object paragraphContent : paragraph.getContent()) {
                paragraphContent = XmlUtils.unwrap(paragraphContent);

                if (paragraphContent instanceof R) {
                    R run = (R) paragraphContent;

                    // Iterate through the content of the run to find text nodes.
                    for (Object runContent : run.getContent()) {
                        runContent = XmlUtils.unwrap(runContent);

                        if (runContent instanceof Text) {
                            Text textElement = (Text) runContent;
                            // Extract the text and perform the replacement.
                            String originalText = textElement.getValue();
                            if (data.get(originalText) instanceof String updatedText) {
                                textElement.setValue(updatedText);
                            }

                        }
                    }
                }
            }
        }
    }
}


  • 我们实现了标题占位符替换器的功能
public class HeaderPlaceholderReplacer implements PlaceholderReplacer{
    @Override
    public void replacePlaceholders(WordprocessingMLPackage wordMLPackage, Map<String, Object> data) {
        HeaderPart headerPart = wordMLPackage.getHeaderFooterPolicy().getDefaultHeader();
        for (Object content : headerPart.getJaxbElement().getContent()) {
            replacePlaceholderInContent(content, data);
        }
    }
}
  • 我们实现了正文占位符替换器
public class BodyPlaceholderReplacer implements PlaceholderReplacer {

    @Override
    public void replacePlaceholders(WordprocessingMLPackage wordMLPackage, Map<String, Object> data) {
        MainDocumentPart mainDocumentPart = wordMLPackage.getMainDocumentPart();
        for (Object content : mainDocumentPart.getJaxbElement().getBody().getContent()) {
            replacePlaceholderInContent(content, data);
        }
    }
}
  • 我们实现了表格占位符替换器
public class TablePlaceholderReplacer implements PlaceholderReplacer {

    @Override
    public void replacePlaceholders(WordprocessingMLPackage wordMLPackage, Map<String, Object> data) {
        try {
            MainDocumentPart documentPart = wordMLPackage.getMainDocumentPart();
            List<Object> tables = documentPart.getJAXBNodesViaXPath("//w:tbl", true);
            for (int i = 0; i < tables.size(); i++) {
                replaceTablePlaceHolder(i, tables.get(i), data);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private void replaceTablePlaceHolder(int index, Object tableObj, Map<String, Object> data) {
        org.docx4j.wml.Tbl table = (org.docx4j.wml.Tbl) XmlUtils.unwrap(tableObj);
        List<Map<String, String>> tableData = (List<Map<String, String>>) data.get("${table" + index + "}");
        List<Object> rows = (List<Object>) XmlUtils.unwrap(table.getContent());
        Tr templateRow = (Tr) rows.get(1);
        for (Map<String, String> rowData : tableData) {
            Tr newRow = XmlUtils.deepCopy(templateRow);
            replaceRowPlaceholders(newRow, rowData);
            table.getContent().add(newRow);
        }
        table.getContent().remove(templateRow);
    }

    public static void replaceRowPlaceholders(Tr row, Map<String, String> rowData) {
        for (Object obj : row.getContent()) {
            obj = XmlUtils.unwrap(obj);
            if (obj instanceof Tc) {
                Tc cell = (Tc) obj;
                replaceTextInCell(cell, rowData);
            }
        }
    }

    private static void replaceTextInCell(Tc cell, Map<String, String> data) {
        // Iterate through all content elements of the cell.
        for (Object content : cell.getContent()) {
            content = XmlUtils.unwrap(content);

            // If the content is a paragraph, iterate through its runs.
            if (content instanceof P) {
                P paragraph = (P) content;

                // Iterate through each run inside the paragraph.
                for (Object paragraphContent : paragraph.getContent()) {
                    paragraphContent = XmlUtils.unwrap(paragraphContent);

                    if (paragraphContent instanceof R) {
                        R run = (R) paragraphContent;

                        // Iterate through the content of the run to find text nodes.
                        for (Object runContent : run.getContent()) {
                            runContent = XmlUtils.unwrap(runContent);

                            if (runContent instanceof Text) {
                                Text textElement = (Text) runContent;

                                // Extract the text, perform the replacement.
                                String originalText = textElement.getValue();
                                String updatedText = data.get(originalText);

                                // Set the updated text back into the Text element if it has changed.
                                if (!originalText.equals(updatedText)) {
                                    textElement.setValue(updatedText);
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.