我正在使用 Java 17+ 以及 docx4j。我需要阅读包含以下内容的 Word 文档模板 标题带有占位符 ${creationDate},文档正文中有一个表格,其中包含以下信息:
ID | First Name | Last Name | Age | Gender |
-----------------------------------------------------------
${id} | ${firstName} | ${lastName} | ${age} | ${gender} |
并生成一个新文档,该文档将用应用程序中的数据替换占位符。 在表中,我们可能有多个人,因此我们需要实现 docx4j 将一行替换为占位符,以表示我们可能拥有的多行数据。
这起初看起来很简单,但我预期花了更长的时间。我使用 docx4j 来实现这个问题,并使用 StackOverflow 来记录这个解决方案。
如果有人有更好的方法来做到这一点,我将奖励其作为已接受的答案。
首先添加 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);
}
}
}
}
}
}
}
}
}