1. Overview

The poi-tl library is an open-source Java library based on Apache POI. It simplifies generating Word documents using templates. The poi-tl library is a Word template engine that creates new documents based on Word templates and data.

We can specify styles in the template. The document generated from the template persists in the specified styles. Templates are declarative and purely tag-based, with different tag patterns for images, text, tables, etc. The poi-tl library also supports custom plug-ins to structure the documents as required.

In this article, we’ll go through different tags we can use in the template and the use of custom plugins in the template.

2. Dependencies

To use the poi-tl library, we add its maven dependency to the project:

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

We can find the latest version of the poi-tl library in Maven Central.

3. Configuration

We use the ConfigureBuilder class to build the configuration:

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

The template file is in the Word .docx file format. First, we compile the template using the compile method from the XWPFTemplate class. The template engine compiles the template and renders the templateData accordingly in a new docx file. Here writeAndClose creates a new file and writes the specified data in the format specified in the template. The templateData is an instance of HashMap with String as key and Object as value.

We can configure the template engine as per our liking.

3.1. Tag Prefix and Suffix

The template engine uses curly braces {{}} to denote tags. We can set it to ${} or any other form we want:

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

3.2. Tag Type

By default, the template engine has identifiers defined for template tags, e.g., @ for image tag, # for table tag, etc. The identifiers for tags can be configured:

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

3.3. Tag Name Format

Tag names support a combination of different characters, letters, numbers, and underscores by default. Regular expressions are used to configure tag name rules:

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

We will go through configurations like plugins and error handling in further sections.

Let’s assume the default configuration for the remainder of the article.

4. Template Tags

The poi-tl library templates don’t have any variable assignments or loops; they are completely based on tags. A Map or DataModel associates the data to be rendered with the template tags in the poi-tl library. In initial examples, we’ll see Map and DataModel.

The tags are formed by a tag name enclosed in two curly braces:

{{tagName}}

Let’s go through basic tags.

4.1. Text

The text tag represents the normal text in the document. Two curly braces enclose the tag name:

{{authorname}}

Here we declare a text tag named authorname. We add this to the template.docx file. Then, we add the data value to the Map:

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

The template data renderer replaces the text tag authorname with the specified value, John, in the generated document. Also, style specified like color and bold is applied in the generated document.

4.2. Images

For the image tag, the tag name precedes with @:

{{@companylogo}}

Here, we define companylogo tag of image type. To display the image, we add companylogo to the data map and specify the path of the image to be displayed.

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

4.3. Numbering

For numbered lists, the tag name is preceded with a *:

{{*bulletlist}}

In this template, we declare a numbered list named bulletlist and add list data:

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);

The different numbering formats, like NumberingFormat.DECIMAL, NumberingFormat.LOWER_LETTER, NumberingFormat.LOWER_ROMAN, etc., configure the list numbering style**.**

4.4. Sections

Opening and closing tags mark sections. In the opening tag name precedes with ? and in the closing tag name is preceded with a /:

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

We create a section named students and add a tag name inside the section. We add section data to the map:

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);

4.5. Tables

Let’s generate a table structure in the Word document using a template**. For table structure, the tag name is preceded by a #:**

{{#table0}}

We add a table tag named table0 and use Tables class methods to add table data:

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

The method Rows.of() can define rows individually and add style like a border to the table rows:

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());

Here, table3 has two rows with the row border set to color 4472C4.

Let’s use MergeCellRule to create a cell merging rule:

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

Here, the cell merging rule merges cells 1 and 2 from the second row of the table. Similarly, other customizations for table tag can be used.

4.5. Nesting

We can add a template inside another template, i.e., nesting of templates. For the nested tag, the tag name is preceded with a +:

{{+nested}}

This declares a nested tag in the template with the name nested. Then, we set data to render for nested documents:

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());

Here, we load the template from nested.docx and set rendered data for the nested template. The poi-tl library template engine renders the nested template as a value or data to display for the nested tag.

4.6. Template Rendering Using DataModel

DataModels can also render the data for templates. Let’s create a Person class:

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

We can set the template tag value using the data model:

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

Here we set a data model named person having attributes name and age. The template accesses attribute values using the ‘.’ operator:

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

Similarly, we can use different types of data in the data model.

5. Plugins

Plugins allow us to execute pre-defined functions at the template tag location. Using the plugins, we can perform almost any operation at the desired location in the template. The poi-tl library has default plugins that need not be configured explicitly. The default plugins handle the rendering of text, images, tables, etc.

Also, there are built-in plugins that need to be configured to be used, and we can develop our own plugins too, called custom plugins. Let’s go through built-in and custom plugins.

5.1. Using Built-in Plugin

For comments, the poi-tl provides a built-in plugin for comments in the Word document. We configure comment tags with CommentRenderPolicy:

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

This registers the comment tag as a comment renderer in the template engine.

Let’s see the use of the comment plugin CommentRenderPolicy:

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

The template engine identifies the comment tag and places the specified comment in the generated document.

Similarly, other available plugins can be used.

5.2. Custom Plugin

We can create a custom plugin for the template engine. Data can be rendered in the document as per custom requirements.

To define a custom plugin, we need to implement the RenderPolicy interface or extend the abstract class 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);
    }
}

Here, we create a sample custom plugin with the class SampleRenderPolicy. Then the template engine is configured to recognize custom plugins:

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

This configuration registers our custom plugin with a tag named sample. The template engine replaces the sample tag in the template with the text Sample plugin custom-plugin.

Similarly, we can develop more customized plugins by extending AbstractRenderPolicy.

6. Logging

To enable logging for the poi-tl library, we can use the Logback library. We add a logger for the poi-tl library to logback.xml:

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

This configuration enables logging for package com.deepoove.poi in the poi-tl library:

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

We can go through logs to observe how template tags are resolved and the data is rendered.

7. Error Handling

The poi-tl library supports customizing the engine’s behavior when errors occur.

In several scenarios where tags can’t be calculated, such as when a non-existent variable is referenced in the template or when the cascaded predicate is not a hash, such as {{student.name}} when the value of the student is null, the value of the name cannot be calculated.

The poi-tl library can configure the calculation results when this error occurs.

By default, the tag value is considered to be null. When we need to strictly check whether the template has human error, we can throw an exception**:**

builder.useDefaultEL(true);

The default behavior of the poi-tl library is to clear the tag. If we do not want to do anything with the tag**:**

builder.setValidErrorHandler(new DiscardHandler());

To perform strict validation, throw an exception directly:

builder.setValidErrorHandler(new AbortHandler());

8. Template Generation Template

The template engine can not only generate documents but also generate new templates. For example, we can split the original text tag into a text tag and a table tag:

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);

Here, we configure a tag named title to DocumentRenderPolicy and create a DocumentRenderData object, forming the template structure.

The template engine identifies the title tag and generates a Word document containing the template structured document put in data.

9. Conclusion

In this article, we learned how to create Word documents using features of the poi-tl library templates. We also discussed different types of tags, logging, and error handling with the poi-tl library.

As always, the source code examples are available over on GitHub.