使用 poi-tl 模板生成 MS Word 文档

在本文中,我们学习如何使用 poi-tl 库模板的功能创建 Word 文档。我们还讨论了使用 poi-tl 库的不同类型的标签、日志记录和错误处理。

poi -tl库是一个基于Apache POI的开源 Java 库。它简化了使用模板生成 Word 文档的过程。poi -tl 库是一个 Word 模板引擎,它可以根据 Word 模板和数据创建新文档。

我们可以在模板中指定样式。从模板生成的文档将保留指定的样式。模板是声明性的,并且纯粹基于标签,具有针对图像、文本、表格等的不同标签模式。poi-tl 库还支持自定义插件,以根据需要构造文档。

在本文中,我们将介绍模板中可以使用的不同标签以及模板中自定义插件的使用。

依赖项
为了使用 poi-tl 库,我们将它的 maven 依赖项添加到项目中:

<dependency>
    <groupId>com.deepoove</groupId>
    <artifactId>poi-tl</artifactId>
    <version>1.12.2</version>
</dependency>

我们可以在Maven Central找到最新版本的poi-tl库。

配置
我们使用ConfigureBuilder类来构建配置:

ConfigureBuilder builder = Configure.builder();
...
XWPFTemplate template = XWPFTemplate.compile(...).render(templateData);
template.writeAndClose(...);

模板文件采用 Word .docx文件格式。首先,我们使用XWPFTemplate类中的compile方法编译模板。模板引擎编译模板并在新的 docx 文件中相应地呈现 templateData。此处writeAndClose创建一个 新文件并以模板中指定的格式写入指定的数据。templateData是一个HashMap实例,以String为键,以Object为值。

我们可以根据自己的喜好配置模板引擎。

 标签前缀和后缀
模板引擎使用花括号{{}}来表示标签。我们可以将其设置为${}或任何其他我们想要的形式:

builder.buildGramer("${", "}");


标签类型
模板引擎默认为模板标签定义了标识符,例如图片标签为@ ,表格标签为#等等。标签的标识符可以配置:

builder.addPlugin('@', new TableRenderPolicy());
builder.addPlugin('#', new PictureRenderPolicy());

标签名称格式
标签名称默认支持不同字符、字母、数字、下划线的组合,使用正则表达式配置标签名称规则:

builder.buildGrammerRegex("[\\w]+(\\.[\\w]+)*");

我们将在后续章节中介绍插件和错误处理等配置。

我们假设本文剩余部分均采用默认配置。

模板标签
poi-tl 库模板没有任何变量赋值或循环;它们完全基于标签。Map或DataModel将要呈现的数据与 poi-tl 库中的模板标签关联起来。在初始示例中,我们将看到Map和DataModel。

标签由两个花括号括起来的标签名称组成:

{{tagName}}

让我们了解一下基本标签。

文本
文本标签表示文档中的普通文本。两个花括号括起标签名称:

{{authorname}}

这里我们声明了一个名为authorname的文本标签。我们将其添加到template.docx文件中。然后,我们将数据值添加到Map中:

this.templateData.put("authorname", Texts.of("John")
  .color(
"000000")
  .bold()
  .create());

模板数据渲染器在生成的文档中将文本标记authorname替换为指定值John。此外,在生成的文档中应用指定的样式(如颜色和粗体)。

图像
对于图像标签,标签名称前面带有@:

{{@companylogo}}

这里我们定义了图片类型的companylogo标签,为了显示图片,我们在数据图中添加companylogo,并指定要显示的图片的路径。

templateData.put("companylogo", "logo.png");

编号
对于编号列表,标签名称前面带有 *:

{{*bulletlist}}

在这个模板中,我们声明一个名为bulletlist的编号列表 并添加列表数据:

List<String> list = new ArrayList<String>();
list.add("Plug-in grammar");
// ...
NumberingBuilder builder = Numberings.of(NumberingFormat.DECIMAL);
for(String s:list) {
    builder.addItem(s);
}
NumberingRenderData renderData = builder.create();    
this.templateData.put(
"list", renderData);

不同的数字格式,如NumberingFormat.DECIMAL、NumberingFormat.LOWER_LETTER、NumberingFormat.LOWER_ROMAN等,配置列表编号样式。

章节
开始和结束标签标记部分。开始标签中的名称以?开头,结束标签中的名称以 / 开头:

{{?students}} {{name}} {{/students}}

我们创建一个名为students 的部分,并在该部分内添加标签名称。我们将部分数据添加到地图中:

Map<String, Object> students = new LinkedHashMap<String, Object>();
students.put("name", "John");
students.put(
"name", "Mary");
students.put(
"name", "Disarray");
this.templateData.put(
"students", students);

表格
让我们使用模板在 Word 文档中生成表格结构。对于表格结构,标签名称前面带有 #:

{{#table0}}

我们添加一个名为table0的表格标签,并 使用 Tables类方法添加表格数据:

templateData.put("table0", Tables.of(new String[][] { new String[] { "00", "01" }, new String[] { "10", "11" } })
  .border(BorderStyle.DEFAULT)
  .create());

Rows.of()方法可以单独定义行,并为表格行添加边框等样式:

RowRenderData row01 = Rows.of("Col0", "col1", "col2")
  .center()
  .bgColor(
"4472C4")
  .create();
RowRenderData row01 = Rows.of(
"Col10", "col11", "col12")
  .center()
  .bgColor(
"4472C4")
  .create();
templateData.put(
"table3", Tables.of(row01, row11)
  .create());

这里,table3有两行,行边框设置为颜色4472C4。

让我们使用MergeCellRule来创建单元格合并规则:

MergeCellRule rule = MergeCellRule.builder()
  .map(Grid.of(1, 0), Grid.of(1, 2))
  .build();
templateData.put("table3", Tables.of(row01, row11)
  .mergeRule(rule)
  .create());

此处单元格合并规则从表格的第二行开始合并单元格1和单元格2。同样,也可以使用表格标签的其他自定义。

嵌套
我们可以在一个模板内添加另一个模板,即模板嵌套。对于嵌套标签,标签名称前面带有 +:

{{+nested}}

这会在模板中声明一个名为nested 的嵌套标签。然后,我们设置要为嵌套文档呈现的数据:

List<Address> subData = new ArrayList<>();
subData.add(new Address("Florida,USA"));
subData.add(new Address(
"Texas,USA"));
templateData.put(
"nested", Includes.ofStream(WordDocumentEditor.class.getClassLoader().getResourceAsStream("nested.docx")).setRenderModel(subData).create());

这里,我们从nested.docx 加载模板,并为嵌套模板设置渲染数据。poi-tl 库模板引擎将嵌套模板渲染为嵌套标签要显示的值或数据。

使用 DataModel 进行模板渲染
DataModels 还可以为模板渲染数据。让我们创建一个Person类:

public class Person {
    private String name;
    private int age;
    // ...
}

我们可以使用数据模型设置模板标签值:

templateData.put("person", new Person("Jimmy", 35));


这里我们设置了一个名为 person 的数据模型,该模型具有属性name和age。模板使用'.'运算符访问属性值:

{{person.name}}
{{person.age}}

类似地,我们可以在数据模型中使用不同类型的数据。

插件
插件允许我们在模板标记位置执行预定义函数。使用插件,我们可以在模板中的所需位置执行几乎任何操作。poi-tl 库具有默认插件,无需明确配置。默认插件处理文本、图像、表格等的渲染。

此外,还有需要配置才能使用的内置插件,我们也可以开发自己的插件,称为自定义插件。让我们来看看内置插件和自定义插件。

使用内置插件
对于评论,poi-tl 提供了一个内置插件,用于在 Word 文档中评论。我们使用CommentRenderPolicy配置评论标签:

builder.bind("comment", new CommentRenderPolicy());

这会将评论标签注册为模板引擎中的评论渲染器。

我们看一下评论插件CommentRenderPolicy的使用:

CommentRenderData comment = Comments.of("SampleExample")
  .signature(
"John", "S", LocaleUtil.getLocaleCalendar())
  .comment(
"Authored by John")
  .create();
templateData.put(
"comment", comment);

模板引擎识别注释标签并将指定的注释放置在生成的文档中。

同样,也可以使用其他可用的插件。

自定义插件
我们可以为模板引擎创建自定义插件。数据可以根据自定义要求在文档中呈现。

要定义自定义插件,我们需要实现RenderPolicy接口或扩展抽象类AbstractRenderPolicy:

public class SampleRenderPolicy implements RenderPolicy {
    @Override
    public void render(ElementTemplate eleTemplate, Object data, XWPFTemplate template) {
        XWPFRun run = ((RunTemplate) eleTemplate).getRun(); 
        String text = "Sample plugin " + String.valueOf(data);
        run.setText(textVal, 0);
    }
}

在这里,我们使用 SampleRenderPolicy 类创建一个示例自定义插件。 然后配置模板引擎以识别自定义插件:

ConfigureBuilder builder = Configure.builder();
builder.bind("sample", new SampleRenderPolicy());
templateData.put(
"sample", "custom-plugin");

此配置使用名为sample 的标签注册了我们的自定义插件。模板引擎将模板中的sample标签替换为文本Sample plugin custom-plugin。

类似地,我们可以通过扩展AbstractRenderPolicy来开发更多定制的插件。

日志记录
要为 poi-tl 库启用日志记录,我们可以使用Logback 库。我们在logback.xml中添加 poi-tl 库的记录器:

<logger name="com.deepoove.poi" level="debug" additivity="false">
    <appender-ref ref=
"STDOUT" />
</logger>

此配置启用poi-tl 库中com.deepoove.poi包的日志记录:

18:01:15.488 [main] INFO  c.d.poi.resolver.TemplateResolver - Resolve the document start...
18:01:15.503 [main] DEBUG c.d.poi.resolver.RunningRunBody - {{title}}
18:01:15.503 [main] DEBUG c.d.poi.resolver.RunningRunBody - [Start]:The run position of {{title}} is 0, Offset in run is 0
18:01:15.503 [main] DEBUG c.d.poi.resolver.RunningRunBody - [End]:The run position of {{title}} is 0, Offset in run is 8
...
18:01:19.661 [main] INFO  c.d.poi.resolver.TemplateResolver - Resolve the document start...
18:01:19.685 [main] INFO  c.d.poi.resolver.TemplateResolver - Resolve the document end, resolve and create 0 MetaTemplates.
18:01:19.685 [main] INFO  c.deepoove.poi.render.DefaultRender - Successfully Render template in 4126 millis

我们可以通过日志来观察模板标签是如何解析以及数据是如何呈现的。

错误处理
poi-tl 库支持自定义发生错误时引擎的行为。

在几种无法计算标签的场景中,比如模板中引用了不存在的变量,或者级联谓词不是哈希,比如{{student.name}}当学生的值为空时,无法计算姓名的值。

poi-tl库可以配置发生该错误时的计算结果。

默认情况下,标签值被认为是null。当我们需要严格检查模板是否有人为错误时,可以抛出异常:

builder.useDefaultEL(true);

poi-tl 库的默认行为是清除标签。如果我们不想对标签执行任何操作:

builder.setValidErrorHandler(new DiscardHandler());


要执行严格验证,直接抛出异常:

builder.setValidErrorHandler(new AbortHandler());

模板生成模板
模板引擎不仅可以生成文档,还可以生成新的模板,比如我们可以将原来的文本标签拆分成文本标签和表格标签:

Configure config = Configure.builder().bind("title", new DocumentRenderPolicy()).build();
Map<String, Object> data = new HashMap<>();
DocumentRenderData document = Documents.of()
  .addParagraph(Paragraphs.of(
"{{title}}").create())
  .addParagraph(Paragraphs.of(
"{{#table}}").create())
  .create();
data.put(
"title", document);

这里我们为DocumentRenderPolicy配置一个名为title的标签,并 创建一个DocumentRenderData对象,形成模板结构。

模板引擎识别标题标签,并生成包含放入数据的模板结构化文档的Word文档。