Table of Contents

Underdocx User Guide

This Guide will describe what Underdocx exactly is, how this framework can be used in your project, how templates can be defined and how custom data can be provided for document generation.

About

Document Template Engine

Underdocx is an extremely extendable open source Java framework to manipulate multiple types of Documents, for example ODT text documents (LibreOffice/OpenOffice Writer), ODG Graphics (LibreOffice/OpenOffice Draw), ODP Presentations (LibreOffice/OpenOffice Impress), ODS Spreadsheets (LibreOffice/OpenOffice Calc) and all plain text files as TXT, HTML, Scripts etc. It uses different types of parameterizable placeholders that will be replaced by custom texts, images, tables and even other documents. This allows you to define "components" that can be easily reused. Underdocx also allows you to define custom placeholders to provide custom functionalities. Also documents can be converted directly to PDF if LibreOffice has been installed on the system.

Underdox is a Template Engine or Template Processor. It receives template documents and data and combines it to generate the concrete document. This is a simplified overview that Underdocx does:
Templates and data can be combined by a simple programming interface (API), but Underdocx can also be used as standalone command line tool (CLI).

Open Source

Underdocx is free and uses the MIT License.
The Source Code is hosted at GitHub.
Releases are provided by Maven Central Repository.
This documentation and more information can be found at Underdocx.org

Getting Started

Add dependency

Add Underdocx as dependency to your project, for example in Maven extend your pom.xml:
<dependency>
    <groupId>io.github.winterrifier</groupId>
    <artifactId>underdocx</artifactId>
    <version>0.12.1</version>
</dependency>
When you use Gradle your dependency should look like this
implementation 'io.github.winterrifier:underdocx:0.12.1'
You can also visit Underdocx on Maven Central Repository.
to find more snippets how to include underdocx into your project.

Hello Word Example

Now you can use this code to create a simple template that will be processed by the template engine:
public class HelloWorld {
  public static void main(String[] args) throws IOException {
    OdtContainer doc = new OdtContainer("Hello ${$name}");
    OdtEngine engine = new OdtEngine();
    engine.pushVariable("name", "World");
    engine.run(doc);
    File tmpFile = File.createTempFile("Test_", ".odt");
    doc.save(tmpFile);
    System.out.println("Document created: %s".formatted(tmpFile));
  }
}
This code will create a ODT-document with prefix "Test_" in your temp folder that contains the text "Hello World"

Principles

Technical abstraction

Depending on your software project, multiple teams with different knowledge can be involved when Underdocx is used:
  • The Template documents could be created by customers without knowledge about software development. Technical details can be hidden behind simple placeholders.
  • Additional importable document "fragments" can also be provided and used by the customer or by the software development team that define document fragments as reusable components, similar to components used in web development.
  • The data used to fill the templates can be provided by customer, by the software development team or any third party
  • Software developers are responsible to use the Underdocx API to combine data and templates
  • Software developers can also extend Underdocx with custom placeholders and functionalities to simplify or improve definition of templates

Use Cases

Example Use Case: Reporting

As describe above, Underdocx can be used to create individual software solutions where different roles can apply data and templates through a Web App. This Web App may also allow other users to generate report-documents after selecting data that shall be visualized.

Example Use Case: Web Shop

Underdocx might also be used by software developers to simply generate PDF documents based on templates. In this example a Web App has been implemented. Its Backend uses predefined templates to let Underdocx generate invoice-documents that can be downloaded by the user.

Example Use Case: Translatable Graphics

Underdocx might be used as a translation tool, taking a document or graphics with placeholders that shall be replaced by translations or other images. Underdocx also takes the translation data, provided by translation files, and generates the documents or graphics in multiple translated variants.

Placeholders

Underdocx uses placeholders to replace them with text or other document components. It provides a predefined placeholder syntax and predefined parameterizable placeholders, so called "commands". The default syntax of placeholders is as follows:
${Key}
${Key attr1:value1}
${Key attr1:value1, "attr2":value2}
Actually the syntax is derived from the JSON-syntax, which allows definition of complex attribute values. Here are some example of placeholders that are supported by default:
${Date}
${Date value:"2022-01-05", outputFormat:"dd.MM.yyyy"}
${For value:[{firstName: "Hans", lastName:"Müller"}, {firstName: "Jon", lastName:"Doe"}], $as:"person"}
Underdocx also supports placeholders of different kinds. For example images can be defined to be placeholders for other images. Also Underdocx can be extended to define custom placeholders with different syntax.

Model, Variables and Values

There are three ways how templates can address data that shall be used.
  • Values are directly provided as placeholder attributes that do not use prefixes characters like '*' and '$'.
  • Variables are stored values, added though the engine API but also dynamically created during template processing. The Attribute-Prefix '$' is associated with variables, which means the attribute refers to a variable
  • The Model-tree defines a static data structure the engine can walk through. This data type is optional. The Attribute-Prefix '*' is associated with the model, which means the attribute points to a node of the model-data-tree

Values

Values are placeholder attribute values that are directly defined and used by the placeholder itself, for example this document content:
Example: ${Date value:"2022-01-05" outputFormat:"dd.MM.yyyy"}
will always be replaced by this text:
Example: 05.01.2022
because the value of the date and the output format to use are defined directly in the placeholder as values.

Variables

A simple way to import data to the template engine are the definition of variables that can be accessed by the placeholders. Usually the attribute prefix '$' indicates access to a variable. For example, the given text:
Example: ${Date $value:"dateToShow" outputFormat:"dd.MM.yyyy"}
and the code snippet:
String lastYear = LocalDate.now().minusYear(1)
          .format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
engine.pushVariable("dateToShow", lastYear);
will generate a document with the date one year ago, which has been stored in the variable "dateToShow". For example:
Example: 19.01.2024

Variable referencing Variable

In some cases it might be useful that a variable stores the name/address of an other variable. This variable reference can be resolved with prefix '$$'. The following example stores the value "Hello World" in variable "out", and Variable "reference" stores the variable name "out". The prefix '$$' is used to resolve "reference" and to print out "Hello World" directly.
${Push key:"out", value:"Hello World"}
${Push key:"reference", value:"out"}
${String $$value:"reference"}

Model

The model is a static hierarchical data structure the template engine can navigate through during template processing. It has one unnamed root node, therefore it is not possible to register multiple models. Existing tree nodes won't usually change their value. The Template Engine always has a current position within the model tree that may change during template processing (for example in for-loops). References to model nodes are always relatively to the engines current model tree position. The usage of models is optional, you might also use variables. This example shows how to register a model and how to address a node:
Example: ${Date *value:"persons[1].birthday" outputFormat:"dd.MM.yyyy"}
and this code snippet:
DataNode model = new MapDataNode("""
          {
              persons: [
                  {birthday: "1975-05-14"},
                  {birthday: "2021-03-07"}
              ]
          }
      """);
engine.setModel(model);
will always generate a document with content:
Example: 07.03.2021
In this example, the model is defined by using the JSON syntax, which is converted to a data-tree using the MapDataNode-class. The *value parameter uses the model-path-syntax to address a certain node in the model data tree.

DataPath-Syntax

Variables and models can store hierarchical data. The DataPath-Syntax can be used to address certain (sub-)nodes in the tree, or even to address parent nodes. There is an overview of characters that have a special meaning:
Character Description Example Explanation
. introduces the next hierarchy level a.b access to the value "b" which is a child node of "a"
[ ] selects an item of a list persons[1] access to the second item of a list named "persons"
^ selects the root node x^a.b access to node "b" which is child of node "a". Node "x" will be ignored
< selects the parent node a.b.c<d access to node "d" which is child of "b"

API Introduction

Document Types

OdtContainer / OdtEngine

OdtContainer instances represent an ODT document which can be modified by the OdtEngine or by user. OdtContainer provides multiple constructors to load or create documents.
public class Main {
  public static void main(String[] args) throws IOException {
    // creates an empty document
    new OdtContainer(); 
    // creates a document with two paragraphs "Hello World" and "How are you?"
    new OdtContainer("Hello World\nHow are you?"); 
    // loads an ODT file
    new OdtContainer(new File("file.odt"));
  }
}
OdtEngine is the main class to process a ODT document, exchange its placeholder and to register custom extensions to provide more placeholders or functionalities. This example registers variable "name" at the engine and processes a text document containing a single placeholder. A file is created that contains text "Hello World"
public class Main {
  public static void main(String[] args) throws IOException {
   OdtContainer doc = new OdtContainer("Hello ${$name}");
   OdtEngine engine = new OdtEngine();
   engine.pushVariable("name", "World");
   engine.run(doc);
   File tmpFile = File.createTempFile("Test_", ".odt");
   doc.save(tmpFile);
  }
}

OdgContainer / OdgEgnine

OdgContainer and OdgEngine are responsible for ODG Graphics documents (LibreOffice Draw). These documents consists of pages that contain multiple graphic elements. The Engine will process the contained placeholders from first to last page and from background to foreground. Tip: Create an initial page that defines some initial useful ${Alias} commands and ${For}-Loops referring tables by their names and let this page remove with help of the ${Remove} command.

OdpContainer / OdpEngine

These instances represent are responsible for ODP Presentation documents (LibreOffice Impress). Structure and usage of these classes are quite similar to OdgContainer/OdgEngine

OdsContainer / OdsEngine

OdsContainer instances represent an ODS Spreadsheet document (LibreOffice Calc). It consists of names tables that may contain placeholders in their cells. The OdsEngine will process the contained placeholders from first to last table from left to right and from top to bottom. It supports all commands except ${Import} and ${Export} Tip: Create an initial spreadsheet that defines some initial useful ${Alias} commands and ${For}-Loops referring tables by their names and let this spreadsheet remove with help of the ${Remove} command.

TxtContainer / TxtEngine

TxtEngine and OdgEngine are responsible for all types of plain text files, for example .txt, .html, .sh etc. The TxtEngine supports all commands except ${Export}, ${Image} and ${Remove}. The ${For}-command does not supported attributes rowNumber and listItem

Resource

Resource is an interface that represents data that can be loaded. This interface is important for command like ${Import}, ${Export} and ${Image} that load images or other documents to include them into the template. There are default implementation to convert URLs, Files or binary data into Resource-instances
// Creates a Resource instance based on a file
Resource resource = new Resource.FileResource(String file);
// Creates a Resource instance based on a file
Resource resource = new Resource.FileResource(File file);
// Creates a Resource instance based on binary data
Resource resource = new Resource.DataResource(byte[] data);
// Creates a Resource instance based on an URI/URL
Resource resource = new Resource.UriResource(String uri);
// Creates a Resource instance based on an URI/URL
Resource resource = new Resource.UriResource(URI uri);
// Creates a Resource instance based on a Base64 encoded String
Resource resource = new Resource.Base64Resource(String b64Data);      

DataNode

All type of values (model and variables) are stored as a tree structure that implement the interface DataNode. You can write your own implementation to provide data to the engine. But there are also some predefined implementation you can easily use, such as MapDataNode, DataTreeBuilder and ReflectionDataNode

MapDataNode

Use MapDataNode to manually register values in a map. These values might also be maps, lists or single (leaf) values. MapDataNode also allows to convert JSON text into DataNodes. The following example uses a JSON to create a DataNode that is stored as variable "persons". The resulting document will contain text "Hello Jenny"
public class Main {
  public static void main(String[] args) throws IOException {
    DataNode persons = new MapDataNode("""{
      persons: [
        {name: "Hans"},
        {name: "Jenny"}
      ]}
    """);
    OdtContainer doc = new OdtContainer("Hello ${$persons[1].name}");
    OdtEngine engine = new OdtEngine();
    engine.pushVariable("persons", persons);
    engine.run(doc);
    File tmpFile = File.createTempFile("Test_", ".odt");
    doc.save(tmpFile);
  }
}

DataTreeBuilder

The DataTreeBuilder provides a fluent API to build up a tree data structure. It helps to construct structures that can not be expressed by JSON, for example structures that contains non-primitive objects. The next example create a list of persons that contain Resource-objects representing images.
private DataNode createPersonsData() throws IOException {
  Resource resource1 = new Resource.FileResource("image1.png");
  Resource resource2 = new Resource.FileResource("image2.png");
  DataNode data = DataTreeBuilder
    .beginList()
      .beginMap()
        .add("firstName", "Hans")
        .add("lastName", "Müller")
        .add("birthDate", "1975-02-01")
        .add("imageTitle", "Hans")
        .add("imageName", "hans.png")
        .addObj("imageResource", resource1)
      .end()
       
      .beginMap()
        .add("firstName", "Helene")
        .add("lastName", "Schäfer")
        .add("birthDate", "2009-05-03")
        .add("imageTitle", "Helene")
        .add("imageName", "helene.png")
        .addObj("imageResource", resource2)
      .end()
        
    .end()
  .build();
        
 return data;
}

ReflectionDataNode

ReflectionDataNode provides a convenient way to convert any object dynamically to DataNode instances. Fields and get methods are called by the java reflection API to build up the tree dynamically. Additionally the definition of Resolver allows to dynamically simulate fields that are not provided by the reflected object. This allows injection of additional data to the model node that is not provided by its origin object.
private static class TestClassA {
  public TestClassB b = new TestClassB();
}
      
private static class TestClassB {
  public List<String> getC() {
    return Arrays.asList("Item1", "Item2");
  }
}
      
public static void main (String[] args) {
  String content = """
    ${*b.x}
    ${*b.c[0]}
  """;
  OdtContainer doc = new OdtContainer(content);
  ReflectionDataNode.Resolver resolver = (object, name) -> 
    name.equals("x")
    ? java.util.Optional.of(new LeafDataNode("42"))
    : Optional.empty();
  OdtEngine engine = new OdtEngine();
  engine.setModel(new TestClassA(), resolver);
  engine.run(doc);
  File tmpFile = File.createTempFile("Test_", ".odt");
  doc.save(tmpFile);
}
This snippet will create a document with content:
42
Item1
Reason: ${*b.c[0]} is a placeholder to print out model node addresses by DataPath b.c[0]. Also ${*b.x} is a placeholder to print out model node addresses by DataPath "b.x". The call of engine.setModel(new TestClassA()) creates an instance of TestClassA and converts it into a ReflectionDataNode. This node is now declared as model data. The Additional "resolver" provides value 42 when property "x" is requested by a placeholder. "x" is not part of the reflected classes.

ReferredDataNode

This DataNode-Wrapper takes a Supplier<DataNode> to dynamically provide a DataNode on request.
engine.pushVariable("data",
  DataTreeBuilder
    .beginMap()
      .add("name", "data1")
      .add("value", "0.1")
      .addNode("style", new ReferredDataNode<>(() -> engine.getVariable("green").get()))
    .end()
  .build()
);

Predefined Commands

Underdocx provides multiple predefined placeholders and attributes to enable some basic functions.

${String}-Command

The placeholder with keyword String is used to be replaced with text. The text can be taken from a variable, model or directly from attribute. Attribute value refers to the text value that has to be printed. There are also shortcuts of the command as you can see in the example below. The String command supports multiple attributes. These attributes can also be used in the shortcut versions.
Attribute Type Description
value String or Primitive Text to be printed. When value-Attribute is missing, current model node will be printed
onNull String of "skip", "fallback", "empty", "deletePlaceholderKeepParagraph", "deletePlaceholderDeleteEmptyParagraph", "deletePlaceholderDeleteParagraph", "keepPlaceholder" Strategy how to to react on missing data
onEmpty String of "skip", "fallback", "empty", "deletePlaceholderKeepParagraph", "deletePlaceholderDeleteEmptyParagraph", "deletePlaceholderDeleteParagraph", "keepPlaceholder" Strategy how to react on empty data
onError String of "skip", "fallback", "empty", "deletePlaceholderKeepParagraph", "deletePlaceholderDeleteEmptyParagraph", "deletePlaceholderDeleteParagraph", "keepPlaceholder" Strategy how to react on invalid data
fallback String text to use when fallback strategy is applied
markup true / false text contains markup tags, e.g. <b> for bold
rescan true / false text may contain placeholder itself to be interpreted
templateCell String Use this attribute when the command is used within table cell. This attribute contains the address (table name and coordinates) to a cell that is formatted as expected. The template cell might be located in a different (temporary) table. For example templateCell:"TmpTable.D1"
Here is an example document with placeholder and its expectations when the code below is executed
Placeholder Example Expectation Explanation
${String value:"Hello World"} Hello World Prints out the attribute value
${String $value:"text"} Variable Prints value of variable "text"
${$text} Variable Prints value of variable "text" (shortcut)
${String *value:"text"} Model Prints value of model property "text"
${*text} Model Prints value of model property "text" (shortcut)
${*markupText} <b>bold</b> <i>italic</i> <u>underline</u> <font size="20">large</font> Prints value of model property "markupText", does not interpret markup elements
${*markupText markup:true} bold italic underline large Prints value of model property "markupText", does not interpret markup elements
${*text onNull:"fallback, fallback:"X"} Model Prints the value of model property "text" (which exists)
${*missing onNull:"fallback, fallback:"X"} X Prints "X" because model property "missing" does not exist
${String value:"Y", onEmpty:"fallback", fallback:"Z"} Y Prints "Y" because attribute is not empty
${String value:" ", onEmpty:"fallback", fallback:"Z"} Z Prints "Z" because attribute is empty
public class Main {
  public static void main(String[] args) throws IOException {
    OdtContainer doc = new OdtContainer(is);
      String modelData = """
        {
          text: "Model",
          markupText: "<b>bold</b> <i>italic</i> <u>underline</u> <font
                       size="20">large</font>"
        }
      """;
      OdtEngine engine = new OdtEngine();;
      engine.pushVariable("text", "Variable");
      engine.setModel(new MapDataNode(modelData));
      engine.run(doc);
      doc.createTmpFile("Test", false, null);
  }
}

${For}-Command

Placeholder keyword "For" introduces a loops. There are different loop implementations. You can repeat multiple paragraphs, rows of a table or items of a list. Also there are some differences how to receive the iterable data and how to access the current item during iteration. Every ${For}-Command must have a corresponding ${EndFor} command to define a repeatable area. This is just an overview of provided attributes, see details in the next sections
Attribute Description
value Mandatory, the list of items to walk though.
as makes the current item accessible by this name
tableRow defines which row index or range of rows shall be repeated
rowGroupSize how many rows belong to a single item
tableName you can use the attribute tableName to address the target table
parentTable you can use this attribute to clarify that this loop is inside the table that contains rows to repeat
listItem list item index that shall be repeated

Loops over model, variables or values

Similar to the String command there is a mandatory value-Attribute, that addresses the list of items to walk though. The attribute may refer a variable, a model node or directly use the attribute value. Additionally you can (or maybe have to) use the as attribute to make the current item accessible by a name. The usage of as depends on how the attribute value is used.
Example Result Description
${For value:["A", "B"], $as:"it"}
  ${$index} ${$it}
${EndFor}
0 A
1 B
The list is defined directly in the value attribute. During iteration, a variable "it" is created that stores the current list item. This item is printed out by command ${$it}. The Variable “index” stores the current item index.
${For $value:"list", $as:"it"}
  ${$index} ${$it}
${EndFor}
0 A
1 B
The list is accessed through variable "list". During iteration, a variable "it" is created that stores the current list item. This item is printed out by command ${$it}. The Variable “index” stores the current item index.
${For *value:"list", $as:"it"}
  ${$index} ${$it}
${EndFor}
0 A
1 B
The list is accessed through model property "list". During iteration, a variable "it" is created that stores the current list item. This item is printed out by command ${$it}. Also, the current item becomes the current model position aka current model node. The Variable “index” stores the current item index.
${For *value:"list", *as:"it"}
  ${$index} ${*it}
${EndFor}
0 A
1 B
The list is accessed through model property "list". During iteration, a prefix "it" is registered that represents the current list item. This item is printed out by command ${*it}. Also, the current item becomes the current model position aka current model node. The Variable “index” stores the current item index.
${For *value:"list"}
  ${$index} ${*}
${EndFor}
0 A
1 B
The list is accessed through model property "list". During iteration the current item becomes the current model position aka current model node. When no "as"-attribute is used, the current model node can be printed with command ${*}. The Variable “index” stores the current item index.

Loop of Tablerows

To repeat table rows, a template-table has to be placed between the ${For} and the ${EndFor}-command. The mandatory attribute tableRow defines which row index or range of rows shall be repeated. Also the optional attribute rowGroupSize defines how may of these rows belong to a single list item.

The example below uses the ${Push}-Command to define a variable "data" that contains a list of person objects. The ${For}-command uses this "data"-list to generate a table. tableRow:[2,7] indicates that indices 2 to 7 are used for the loops. Attribute rowGroupSize:3 ensures each item receives 3 rows of the repeatable rows. As you can see in the example, cells of multiple rows can be merged as long they are part of one group. Placeholder ${Counter} prints the current loop iteration
When the table can not be placed between ${For} and ${EndFor} (for example in Spreadsheets), you can use the attribute tableName to address the target table, or you can use parentTable:true to address the table in which the command is located in. In both cases ensure the ${EndFor} command directly follows the ${For} command.
To style table cells use Attribute templaceCell of several commands. It might be useful to store styled cell addresses as variable, that can be used later by the data.

Loop of ListItems

To Loop items of a list use attribute listItem to define a list item index that shall be repeated. Similar to loops over table rows, the ${For} and ${EndFor} command has to surround a "real" list created by the (LibreOffice-)Application. The example below uses the ${Push}-Command to define a variable "data" that contains a list of person objects. The ${For}-command uses this "data"-list to extends the existing list.
Example Expectation
${Push key:“data“, value:[
  {firstName: „Hans“, lastName:“Müller“},
  {firstName: „Johanna“, lastName:“Sommer“},
  {firstName: „Helene“, lastName:“Jäger“}
]}

${For $value:“data“, $as:“person“, listItem:0}
 1. ${$person.firstName} ${$person.lastName}
${EndFor}
  1. Hans Müller
  2. Johanna Sommer
  3. Helene Jäger

${$index} and ${Counter}-Command

Within loops, the current item index will be stores in Variable $index. The first item will be associated with index 0. Because humans are use to count beginning with 1, the Command ${Counter} prints out the index increased by one.

${If}-Command

The ${If}-command is used to hide or show parts of the document. To define the visible area each ${If}-command requires a corresponding ${EndIf}-command. The visibility decision is made by the first attribute, which is a simple or complex condition that depends on variables or model values. A condition is a hierarchical structure of one or multiple condition elements. The next example shows the syntax of these condition elements:
// condition is true only when variable "v" contains value true
$v:true
// condition is false only when variable "v" contains value true
$v:false
// condition is true only when variable "v" contains string "x"          
$v:"x"
// condition is true only when variable "v" contains integer 42
$v:"42
// condition is true only when variable "v" is null or does not exist
$v:null
// condition is true only when variable "v" is an empty list
$v:[]

// condition is true when condition element within { } is false, otherwise true
not:{ ... }
// condition is only true when all comma separated condition elements
//within [ ] are true, otherwise false
and:[ ... ]
// condition is only true when at least one of the comma separated condition 
// elements within [ ] is true, otherwise false
or:[ ... ]
// condition is true when variable x is less then y
less:{$x:y}
// condition is true when variable x is greater then y
greater:{$x:y}
// condition is true when variable x is less or equal y
lessOrEqual:{$x:y}
// condition is true when variable x is greater or equal y
greaterOrEqual:{$x:y}
The next example box shows multiple if statements. The ${Push}-command is used to define some example variables the if conditions will rely on. All "HIDDEN" texts will be invisible when Underdocx would interpret the following statements:
${Push key:"testString", value:"Test"}
${Push key:"testTrue", value:true}
${Push key:"testInt", value:3}

${If $testString:"Test"} VISIBLE ${EndIf}
${If $testString:"xyz"} HIDDEN ${EndIf}
${If $testTrue:true} VISIBLE ${EndIf}
${If $testTrue:false} HIDDEN ${EndIf}
${If not:{$testTrue:false}} VISIBLE ${EndIf}

${If and:[{$testString:"Test"},{$testTrue:true}]}
  VISIBLE
${EndIf}

${If and:[{$testString:"Test"},{not:{$testTrue:true}}]}
  HIDDEN
${EndIf}

${If or:[{$testString:"Test"},{$testTrue:false}]}
  VISIBLE
${EndIf}

${If or:[{$testString:"x"},{$testTrue:false}]}
  HIDDEN
${EndIf}

${If less:{$testInt:2}}
  VISIBLE
${EndIf}

${If less:{$testInt:3}}
  HIDDEN
${EndIf}

${If greater:{$testInt:4}}
  VISIBLE
${EndIf}

${If greater:{$testInt:3}}
  HIDDEN
${EndIf}

${Import}-Command

This command enables creation of reusable document-components that are imported into the main document. Imported document are usual document that also may contain placeholders that will be interpreted after the document has been imported. Note that only the main content of a component will be imported, footer or headers fill be ignored. ${Import} is currently not available for ODS documents. $Import offers different attributes to address a document that shall be imported:
Attribute Description
uri Attribute refers to a String that contains an URI to the document
data Attribute refers to a byte-Array that contains the binary data of the document
base64 Attribute refers to a Base64 encoded String that contains the binary data of the document
resource Attribute refers to a Resource-object that represents the document
page Can be used for ODP/ODG templates: select the page name to import. The content of the page will be imported to the page that hold this command. When this attribute is missing all pages are imported.
toPage Optional: for ODG and ODP files select the target page name to import into
beginFragmentRegex For TXT files this optional regular expression marks the start of a text fragment to import. The marked line is excluded
endFragmentRegex For TXT files this optional regular expression marks the end of a text fragment to import. The marked line is excluded
This example code create a document with different Import-Placeholders. All of them will be replaced by the content of the same document:
public class Main {
  public static void main(String[] args) throws IOException {
      File file = new File("/home/User/doc1.odt");
      String content = """
              ${Import uri:"%s"}
              ${Import $data:"data"}
              ${Import $resource:"resource"}
              """.formatted("file:///home/User/doc1.odt");
      OdtContainer doc = new OdtContainer(content);
      OdtEngine engine = new OdtEngine();
      byte[] data = Files.readAllBytes(Paths.get(file));
      engine.pushLeafVariable("data", data);
      Resource resource = new Resource.FileResource(file);
      engine.pushLeafVariable("resource", resource);
      engine.run(doc);
      doc.createTmpFile("Test", false, null);
      doc.save(tmpFile);
  }
}

${Export}-Command

The ${Export}-command is quite similar the ${Import}-command, but it addresses a master-document that shall import the current document. That means the footer and header of the master document will be visible in the final document. When Attribute name and placeholder ${ExportTarget} are not used, the content of the current document will be appended to the master document. ${Export} is currently only available for ODT documents. Exported pages in ODP and ODG document will be inserted after last page or after the page marked with ${ExportTarget}. If required target pages have to be removed separately, see command Remove.
Attribute Description
uri Attribute refers to a String that contains a URI to the document
data Attribute refers to a byte-Array that contains the binary data of the document
base64 Attribute refers to a base64 encoded String that contains the binary data of the document
resource Attribute refers to a Resource-object that represents the document
name Name of the ${ExportTarget} placeholder in the master document that shall receive this document

${ExportTarget}-Command

The master document may contain a ${ExportTarget}-placeholders that shall receive the content of the current document that contains the ${Export}-command. In this case, both the ${Export} and ${ExportTarget} placeholders have to use the name Attribute with an identical string value.

${Image}-Command

This command is a special placeholder, because it is not a plain textual command. Images placeholder are real images that are placed during template creation. An image becomes a placeholder image by adding the ${Image} as description. This textual part of the placeholder is located in the "Description" field of the "Options"-tab of the "Properties..."-window of the image, which is selectable when you right-click the image.
The textual image command provides following attributes
Attribute Required Description
uri required if not data, base64 or resource has been used Attribute refers to a String that contains an URI to the image to use
data required if not resource, base64 or uri has been used Attribute refers to a byte-Array that contains the binary data of the image
base64 required if not resource, data or uri has been used Attribute refers to a base64 encoded String that contains the binary data of the image
resource required if not data, base64 or uri has been used Attribute refers to a Resource-object that represents the image
mimeType required if data or base64 has been used file extension or MIME type of image
name optional Name of the image to set
keepWidth optional when true, replaced image will have same width as this template image and height will be recalculated. When false, replaced image will have same height as this template image and width will be recalculated. When not set the origin size of the image won't change. This property is ignored for shapes
Note that images must contain unique names.

The image command can also be used to set the background for shapes. Select "Alt Text.." to input the command.
The attribute keepWidth is ignored for this kind of images.

${CreateImage}-Command

Command CreateImage enables creation of new images based on textual placeholders instead of existing images (see ${Image}). To layout a new image by attributes is quite complex and can lead to invalid configurations, therefore try to use the ${Image} command to replace an existing image if possible. On the other hand CreateImage enables dynamic image creation within (dynamic) text and provides more flexibility.
The following attributes are derived from ODF specification, see OpenDocument V1.3 OASIS Standard. Allowed values and attribute combinations will not be documented here.
Attribute Description Link to Spec
uri Location of the image, alternative to resource/base64/data
base64 Base64 encoded data of the image, alternative to resource/uri/data
data Binary data of the image, alternative to resource/uri/base64
resource Image resource object, alternative to uri/base64/data
mimeType file extension or MIME type of image, required if data or base64 has been used
unit Length unit to use for attribute y, x, width, height
name Image name draw:name
desc Image desc svg:desc
wrap specifies how text is displayed around the image style:wrap
anchor specifies how the image is bound to a text document text:anchor-type
x specifies a default horizontal position svg:x
y specifies a default vertical position svg:y
width specifies a default width (optional when height is set) svg:width
height specifies a default height (optional when width is set) svg:height
horizontalPos specifies the horizontal alignment style:horizontal-pos
horizontalRel specifies the area against which the horizontal position of the image is positioned style:horizontal-rel
verticalPos specifies the vertical alignment of an image relative to a specific area style:vertical-pos
verticalRel specifies the area against which the vertical position of the image is positioned style:vertical-rel
Example:
ABC${CreateImage
   $resource:"resource",
   anchor:"as-char",
   horizontalPos:"from-left",
   horizontalRel:"page",
   verticalPos:"middle",
   verticalRel:"text",
   width:12,
   unit:"pt",
   name:"Image",
   desc:"Generated Image"
}DEF
creates:

${Date}-Command

This command simplifies printing dates. It uses the value-attribute to read a date string from a variable, model property or from attribute directly. This command can also be used to print the current date, in this case don't use the value-attribute
Attribute Description
value date string. If not set, the current date will be used
inputFormat expected date-format of the string addressed by attribute "value". Default format is "yyyy-MM-dd" See Javas DateTimeFormatter about details of the format syntax.
outputFormat preferred date format to display after replacement. Default format is "yyyy-MM-dd" See Javas DateTimeFormatter about details of the format syntax.
lang the language code that might influence the date formatting, e.g. "en-US" or "de-DE"
templateCell Use this attribute when the command is used within table cell. This attribute contains the address (table name and coordinates) to a cell that is formatted as expected. The template cell might be located in a different (temporary) table. For example templateCell:"TmpTable.D1"
Examples:
// prints the current date, e.g. 2025-01-25
${Date}

// prints the current date, e.g. 25.01.2025
${Date outputFormat:"dd.MM.yyyy"}

// prints 2024-12-24             
${Date value:"2024-12-24"}

// prints 24.12.2024  
${Date value:"2024-12-24" outputFormat:"dd.MM.yyyy"}

// prints 2024-12-24
${Date value:"24.12.2024" inputFormat:"dd.MM.yyyy"} 

// prints the date stored in variable "date" 
${Date $value:"date"}

${Time}-Command

This command similar to the ${Date} command. It uses the value-attribute to read a time string from a variable, model property or from attribute directly. This command can also be used to print the current time, in this case don't use the value-attribute
Attribute Description
value time string. If not set, the current date will be used. Ensure the string also the parsable data for date and time, even when you only want to print out the time
inputFormat expected time-format of the string addressed by attribute "value". Default format is "yyyy-MM-dd HH:mm:ss" See Javas DateTimeFormatter about details of the format syntax. Ensure the string contains data for date and time, even when you only want to print out the time
outputFormat preferred time format to display after replacement. Default format is "yyyy-MM-dd HH:mm:ss" See Javas DateTimeFormatter about details of the format syntax.
lang the language code that might influence the date formatting, e.g. "en-US" or "de-DE"
templateCell Use this attribute when the command is used within table cell. This attribute contains the address (table name and coordinates) to a cell that is formatted as expected. The template cell might be located in a different (temporary) table. For example templateCell:"TmpTable.D1"
Examples:
// prints the current time, e.g. 2025-01-25 11:02:45
${Time}

// prints the current time, e.g. 11:02
${Time outputFormat:"HH:mm"}

// prints 2024-12-24 02:03:45            
${Time value:"2024-12-24 02:03:45"}

${Number}-Command

This command allows you to format number outputs with basic calculations.
Attribute Description
value the number to display (model node, variable or attribute)
format how to format the number. Default format is "#,###.##". Please read the Java documentation of DecimalFormat for formatting syntax.
intFormat if value is an Integer, use this optional format string instead of "format"
lang the language code that might influence the date formatting, e.g. "en-US" or "de-DE"
multiplier multiplies the value by this factor, e.g. to calculated percentage (0.2 => 20 %)
summand adds this value to the base value, e.g. to calculate row number from index (0 => 1)
prefix adds a string prefix
suffix adds a string suffix, can be used for units, percentage or currency, e.g. 2.30 => 2.30 €
templateCell Use this attribute when the command is used within table cell. This attribute contains the address (table name and coordinates) to a cell that is formatted as expected. The template cell might be located in a different (temporary) table. For example templateCell:"TmpTable.D1"
useModified Use this attribute when the command is used within spreadsheet table cell (ODS). If set to true, the modified value (by multiplier and summand) will be stored as new cell value, Otherwise the origin value will be stored as cell value. For example use useModified:false and multiplier:100 for percentage calculation, use summand:1 and useModified:true to increase current index by 1
Examples:
// prints: 2.00€
${Number value:2, format:"#.00", suffix: "€"}

// prints: 20%
${Number value:0.2, multiplier: 100, suffix: "%"}

${Join}-Command

Use this command to combine a list of strings and separate them with a separator. You can also limit the output or set and special separator to use before the last item.
Attribute Description
value the list of ModeNodes. Ensure each item provides a valid toString implementation
separator optional, sets the separator to use between the items, default value is ", "
lastSeparator optional, sets the last before the last item, default value is value of separator
limit optional, sets the maximum number of items to print, default value is -1, which means no limit
truncated optional, sets suffix text to use when limit value has been exceeded
Examples:
// prints: Bibi, Tina, Amadeus, Sabrina
${Join value:["Bibi", "Tina", "Amadeus", "Sabrina"]}
          
// prints: Bibi, Tina, Amadeus und Sabrina             
${Join value:["Bibi", "Tina", "Amadeus", "Sabrina"], lastSeparator:" und "}
          
// prints: Bibi, Tina usw.
${Join value:["Bibi", "Tina", "Amadeus", "Sabrina"], limit: 2, truncated:" usw."}

${Alias}-Command

An alias is a placeholder representing another placeholder and its parameters. It can be used to reduce complexity or to define new default parameters of the replaced placeholder. To support alias placeholders an alias definition has to be registered first. There are two ways to register new alias definitions, the ${Alias} command and the method registerAlias of the Engine API, which might be the best way to hide complexity. In case you prefer alias definition in the document, the ${Alias} command provides these attributes:
Attribute Description
key placeholders that have this key will be replaced with "replaceKey"
replaceKey new key to use when a placeholder with "key" has been found
attributes This attribute is optional and contains an object of the default property key-value-pairs
attrReplacements This attribute is optional and contains an object of key-value-pairs of attribute shortcuts
An alias placeholder may still use parameters, even when the registered alias definition already defines the same parameters as default values. In this case the parameters of the alias definition are overwritten.
// alias definition
${Alias 
   key: "myDate", 
   replaceKey: "Date", 
   attributes: {outputFormat: "dd.MM.yyyy"}
   attrReplacements: {
     of:"outputFormat", 
     if:"inputFormat",
     v:"value"
   }
}

// prints current date similar to 2025-02-25
${Date}

// prints current date similar to 25.02.2025
${myDate}

// prints current date similar to 2025/02/25
${myDate of:"yyyy/MM/dd"}
          
// prints: 12.12.2024
${myDate v:"2024-12-12"}

${Replace}-Command

This command is similar to ${Alias} command, but will replace all placeholders with a certain key by a provided text. All parameters of the replaced placeholder will be ignored.

You can use this command to replace single short commands by a sequence of other more complex commands
Attribute Description
key key of placeholders to replace
value text to use for replacements. Don't forget escape sequences when replacing with other placeholders
// replacement definition
${Replace
  key:"x",
  value:"${String value:\"Hallo\"} ${String value:\"Welt\"}"
}

// prints: Hallo Welt
${x}

${Remove}-Command

Removes the paragraph, page, text-box or table which contains this placeholder. In ODS, ODG and ODS documents you could use this command to create a temporary area with commands that will be removed in the final document. In this temporary area you can define ${Alias} commands or ${For} statements addressing remote tables. When attribute "name" is used, the table or page that has this name will be removed.
Attribute Description
type type of element that shall be removed, allowed values are page, table, box and paragraph
now if true, removal will be executed immediately. All following placeholders within the removed element will not be executed. Use now:false to let the template engine remove the element later
name Optional: Remove the table or page that has this name. If not set the parent page, text-box or table will be removed that contains this placeholder

${Clone}-Command

Clones a page (slide) in ODG/ODP documents or a table in ODS documents.
Attribute Description
name name of table or page (slide) to clone
newName new name to use for the cloned table or page
insertBefore optional: name of table or page to insert the cloned element before

${Copy}-Command

Copies the content of a page (slide) in ODG/ODP documents into another
Attribute Description
from name of table or page (slide) to copy from
to name to target page

${Create}-Command

Creates a new empty page (ODG, ODP).
Attribute Description
name name of the new page (slide) to create
type type has to be "page"
master master of the page (slide), default is "Default"
insertBefore optional: name of page to insert the created element before

${PageStyle}-Command

In ODT document, page breaks, page-orientation-changes and new page numbers can be forced with the ${PageStyle}-command. To understand this command it is important to understand how ODT handles pages breaks with page style changes. First of all there are to types of page break - "Before": The page break will be inserted before this paragraph/placeholder begins (default) - "After" The page break will be inserted after this paragraph (not recommended)

Only page breaks of type "Before" can define/change the page style to use for the next page. And only page breaks with explicit page style can change the page number. You can try it in LibreOffice Write by manually editing the page break:
All available page styles can be shown or edited in the styles view (Press F11 and select button "Page Styles"). Here you can also add custom page styles with different header and footers.
The ${PageStyle}-command acts like this LibreOffice Writer component:
Therefore you can use these attributes:
Attribute Values Description
pageBreakAfter (ODT only) true/false if true, a page break is inserted after this paragraph
pageBreakBefore (ODT only) true/false if true, a page break is inserted before this paragraph (recommended)
masterPage (ODT/ODP/ODG) String only allowed when pageBreakBefore is true, sets the new page style for the page after the break
pageNumber (ODT only)r Integer only allowed when masterPage is set, reset the page number for the page after the break
When the ${PageStyle}-command is used in a document that will be exported to a master document by the ${Export}-command, ensure attribute masterPage uses a style name that is defined in the master document.

Command ${PageStyle} can also be used for presentations and graphics (ODP/ODG), but only attribute masterPage is supported.

${Push}-Command / ${Pop}-Command

The ${Push}-command allows definition of variables within the document. It can also be used to copy a variables or model nodes. Technically spoken all new variables are pushed onto a stack, that means the previous variable value will not be overwritten but becomes hidden by the new one. The ${Pop}-command removes a variable value from the stack and makes the previous value visible again. Underdocx uses this stack to ensure the $index always represent the current index in the current loop, even when loops are nested.

The ${Push}-command provides two mandatory attributes
Attribute Description
key Name of the variable. You can access another variable or model node to read out the name
value Variable value to store. You can access an other variable or model node to read out the value
The ${Pop}-command only support the attribute key to address the variable that shall be removed from the stack.

${Calc}-Command

Use this command to add, subtract, multiply and divide two values and store the result as variable. Here are the attributes of the command:
Attribute Allowed values Description
a Double, Integer or Long First value to calculate with
b Double, Integer or Long Second value to calculate with
operator "+", "-", "/", "*" The calculation method
key String Name of variable to store the result
Examples:
${Push key:"i", value:4}

// prints 6
${Calc key:"x", $a:"i", operator:"+", b:2}${$x}

// prints 8.0
${Calc key:"x", a:2.0, operator:"*", $b:"i"}${$x}

${Concat}-Command

You can combine multiple values and add them in a variable as list or as concatenated string
Attribute Allowed values Description
a Number or String First value to concatenate
b Number or String Second value to concatenate
c Number or String Third value to concatenate
d Number or String Fourth value to concatenate
e Number or String Fifth value to concatenate
type "string" or "list" Store elements as list or combine them in a single string
key String Variable name to store the result list or string

${Ignore}-Command

Lets the engine ignore all following commands/placeholders until ${EndIgnore} command is reached.

${Exit}-Command

Stops interpretation of all following placeholders.

${Model}-Command

This command is used to test the current model position / current model node. It is internally used by Underdocx to realize For-Loop by copying the content of the loop multiple times and setting the model position before and after every repeat. In case you want to change the model position the ${Model} command provides these attributes:
Attribute Description
interpret set the model position according to the model path relatively to the current position.
value set the model position according to the absolute model path
activeModelPathPrefix registers a prefix that has to be used in model paths (see *as in for-loops)

Placeholder Styles

You can change the syntax style of placeholders by implementing your own style or by using predefined styles. Changing the placeholder style might help you to edit (text) templates. The TxtEngine can use TxtPlaceholderStyle that provides styles DOUBLE-BRACKETS, XML-COMMENT, CODE-COMMENT and HASH-COMMENT.

XML-Comment Style

Use the XML-Comment style to hide the commands in XML comments.
<html><body>
  <ul>
    <!--${For value:["Hans", "Otto", "Gerda"], $as:"item"}-->
      <li><!--${$item}--></li>
    <!--${EndFor}-->
  </ul>
</body></html>

Double-Brackets Style

This placeholder style is derived from the mustache-template-engine placeholder syntax.
{{Join value:["Bibi", "Tina", "Amadeus", "Sabrina"]}} {{Date value:"2022-02-02"}}

Code-Comment Style

Use the Code-Comment Style to hide the commands in language-comments as used in Java, TypeScript, C++ etc.
public class Test {
    /*!For $value:"entries", $as:"item"!*/
    /*!$item!*/
    /*!EndFor!*/
}

Hash-Comment Style

Use the Hash-Comment Style to hide the commands in language-comments as used in Bash or Python
#!Join value:["Bibi", "Tina", "Amadeus", "Sabrina"]!# #!Date value:"2022-02-02"!#

PDF Generation

You can use Underdocx to convert ODT files into PDF.

Class OdfContainer which is a superclass of OdtContainer provides method writePDF(OutputStream os) so save the document as PDF.

Underdocx uses an existing installation of LibreOffice to convert a document into PDF. Ensure environment variable LIBREOFFICE contains the path of the LibreOffice installation, for example "C:/Program Files/LibreOffice/program/soffice.exe" on windows.

Command Line Interface (CLI)

When Underdocx is fed with the template document and all required data, it can build the final document without use of the API. Unfortunately there is currently no download available for the CLI tool, therefore you need to build the CLI version manually

Build Underdocx as CLI Tool

You need "git", "JDK" and "Maven" to build your CLI variant of underdocx. When these tools are installed type these commands:
git clone https://github.com/winterrifier/underdocx.git
cd underdocx
mvn clean install -P buildCli
After execution there will be a target folder that contains "underdocx-0.12.1-jar-with-dependencies.jar". You can use this jar file as CLI tool.

Usage of CLI

To call Underdocx by CLI use this syntax:
java -jar underdocx-0.12.1-jar-with-dependencies.jar <engineType> <templateFile> <outFile> [dataJsonFile1] [dataJsonFile2] ...
For example:
java -jar underdocx-0.12.1-jar-with-dependencies.jar ODT template.odt out.odt data.json
Note: It is also possible to add custom Placeholder/Command implementations (see section Custom Extensions). Implement interface EngineProvider, include your classes to the classpath, execute UnderdocxEngineRunder as main class and use the qualified class name of your provider as first argument (instead of ODT)

Underdocx Json format

The optional Json files (last arguments) contains all data the Template Engine requires to fill the template document. It may contain the data model, variables, alias definitions and String replacements. The schema is as follows:
{
  variables: { key1:value1, key2:value2, ... }, // values may be complex JSON values
  model: {...}, // model data tree
  stringReplacements:{ key1:repl1, key2:repl2, ... }, // simple placeholder replacements
  alias: [
    { 
      key:key1, 
      replaceKey:replaceKey1, 
      attributes: {att1:value1, att2:value2, ...},
      attrReplacements: {attr1:replaceAttr1, att2:replaceAttr2, ...}
    }, ...
  ]
}
Note: When multiple data files are used, the data of a json file might overwrite data of a previous file, for example if variables have the same name or multiple models are specified.

Custom Extensions

Underdocx has been designed of extendability. You can change the syntax of placeholders and add custom placeholders and functionalities in different ways.

Custom Placeholders

A simple way to add custom placeholders is to add a new custom "key" as command that will be replaced by another more complex command. This allows you to hide complexity behind simple placeholders. For example, you can import another sub document using the ${Import} command and hide the functionality bind the custom command ${ReportTable}

The engine provides method registerStringReplacement to register such replacements. Note: All replacements will be performed in the document when the engine detects the first alias placeholder.

The following example creates a custom ${Begin}- and ${End}-command that will be replaced by a predefined more complex ${For} and ${EndFor} command.
String documentStr = """
            ${Begin}
              ${$index} ${$element} 
            ${End}
          """;
OdtContainer doc = new OdtContainer(documentStr);
OdtEngine engine = new OdtEngine();
engine.registerStringReplacement("Begin", 
  "${For value:[\"A\", \"B\", \"C\"], $as:\"element\"} ");
engine.registerStringReplacement("End", "${EndFor}");
engine.run(doc);

Custom Handlers

CommandHandlers can be written and registered at the engine. They receive detected placeholders and an API to modify the document DOM if they "feel responsible" for the provided placeholder. Usually these CommandHandlers read out the attributes of the placeholder and pass the collected information to a so called Modifier that is responsible to manipulate the internal document DOM.

Example: Custom ${Add}-Command

The next example defines a custom CommandHandler hat adds two attribute a and b and stores the sum in a variable defined by attribute target. The AddCommandHandler extends class AbstractTextualCommandHandler and uses ExtendedDataPicker instances that read out the attributes of the placeholder. The result is stored in the variable stack with help of the provided DataAccess instance. The ${Add} placeholder itself will be deleted by the DeletePlaceholderModifier that receives the DOM-Node that represent the handled placeholder. The AddCommandHandler is registered at the engine by calling the registerParametersCommandHandler method.

This example creates a document that initially contains the ${Add} -command to add two values (2+3). After execution the document will just contain the calculated sum (5).
public class AddExample {

  private static class AddCommandHandler extends AbstractTextualCommandHandler<OdtContainer, OdfTextDocument> {
      protected AddCommandHandler() {
          super(new Regex("Add"));
      }

      @Override
      protected CommandHandlerResult tryExecuteTextualCommand() {
          int aValue = getAttr(new IntegerDataPicker().expectedAttr("a"));
          int bValue = getAttr(new IntegerDataPicker().expectedAttr("b"));
          String target = getAttr(new StringConvertDataPicker().expectedAttr("target"));
          int result = aValue + bValue;
          dataAccess.pushVariable(target, new LeafDataNode<>(result));
          new OdfDeletePlaceholderModifier().modify(selection.getNode(), DeletePlaceholderModifierData.DEFAULT);
          return CommandHandlerResult.EXECUTED_PROCEED;
      }
  }

  public static void main(String[] args) throws IOException {
      String content = """
              ${Add a:2, b:3, target:"sum"}
              ${$sum}
              """;
      OdtContainer doc = new OdtContainer(content);
      OdtEngine engine = new OdtEngine();
      engine.registerParametersCommandHandler(new AddCommandHandler());
      engine.run(doc);
      File tmpFile = File.createTempFile("Test_", ".odt");
      doc.save(tmpFile);
      System.out.println("Document created: %s".formatted(tmpFile));
  }
};

Custom Syntax

To define custom placeholders with custom syntax, you have to implements the PlaceholdersProvider and it's PlaceholderProvider.Factory interfaces. There are some base classes you can reuse as done in the example below. For example the RegexExtractor finds text sequences in the document by passing a regular expression, the TextualPlaceholderToolkit simplifies placeholder node handling, and the AbstractOdfPlaceholdersProviderFactory uses these helpers to create the PlaceholdersProvider.

The example below also defines a custom UpperCaseCommandHandler that accepts placeholders provided by the custom MyPlaceholdersProvider. It provides all word beginning with < and ending with >. The UpperCaseCommandHandler takes the placeholder payload, converts it in uppercase, and replaces the placeholder with the converted text.

The main method of the example creates a document that initially contains text Hello <name>, which will modified to Hello NAME.
public class CustomPlaceholderExample {

  private static class MyPlaceholdersProvider extends AbstractOdfPlaceholdersProviderFactory<OdtContainer, String, OdfTextDocument> {

      private static final Regex regex = new Regex("\\<\\w+\\>");
      
      @Override
      public EncapsulatedNodesExtractor getExtractor() {
          return new RegexExtractor(regex, getTextNodeInterpreter());
      }

      @Override
      public Codec<String> getCodec() {
          return new Codec<>() {
              @Override
              public String parse(String string) {
                  if (string.startsWith("<") && string.endsWith(">"))
                      return string.substring(1, string.length() - 1);
                  throw new RuntimeException("parse error");
              }

              @Override
              public String getTextContent(String data) {
                  return "<" + data + ">";
              }
          };
      }
  }

  private static class UpperCaseCommandHandler implements CommandHandler<OdtContainer, String, OdfTextDocument> {

      @Override
      public CommandHandlerResult tryExecuteCommand(Selection<OdtContainer, String, OdfTextDocument> selection) {
          selection.getPlaceholderToolkit().ifPresent(toolkit -> {
              Node placeholderNode = selection.getNode();
              String word = toolkit.parsePlaceholder(placeholderNode);
              toolkit.replacePlaceholderWithText(placeholderNode, word.toUpperCase());
          });
          return CommandHandlerResult.EXECUTED_PROCEED;
      }
  }

  public static void main(String[] args) throws IOException {
      String content = "Hello <name>";
      OdtContainer doc = new OdtContainer(content);
      OdtEngine engine = new OdtEngine();
      engine.registerCommandHandler(new MyPlaceholdersProvider(), new UpperCaseCommandHandler());
      engine.run(doc);
      File tmpFile = File.createTempFile("Test_", ".odt");
      doc.save(tmpFile); // Expectation: Document containing "Hello NAME"
  }
}