Add the following dependencies to your project's pom.xml to anable Word document manipulation:
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-scratchpad</artifactId>
<version>3.16</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
Define placheolders in your .docx template using curly braces, to example {userName}. The Java logic replaces these keys by matching them against the keys in a provided Map object.
package com.example.docs;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.util.Units;
import org.apache.poi.xwpf.usermodel.*;
import org.apache.poi.xwpf.usermodel.Document;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class DocxGenerator {
private XWPFDocument doc;
public DocxGenerator(InputStream templateStream) throws IOException {
this.doc = new XWPFDocument(templateStream);
}
public void writeToStream(OutputStream outputStream) throws IOException {
doc.write(outputStream);
}
public void processTemplate(Map<String, Object> dataModel) {
if (!dataModel.containsKey("staticVars")) {
System.err.println("Data model missing 'staticVars' map.");
return;
}
Map<String, Object> staticVars = (Map<String, Object>) dataModel.get("staticVars");
List<IBodyElement> elements = doc.getBodyElements();
int tableCursor = 0;
int paraCursor = 0;
for (int i = 0; i < elements.size(); i++) {
IBodyElement element = elements.get(i);
if (element.getElementType() == BodyElementType.TABLE) {
XWPFTable currentTable = element.getBody().getTableArray(tableCursor);
if (currentTable != null) {
String tableContent = currentTable.getText();
if (tableContent.contains("##{loopTable")) {
handleLoopTable(currentTable, dataModel, staticVars);
} else if (tableContent.contains("{")) {
applyTableReplacements(currentTable, staticVars);
} else {
cloneStaticTable(currentTable);
}
tableCursor++;
}
} else if (element.getElementType() == BodyElementType.PARAGRAPH) {
XWPFParagraph paragraph = element.getBody().getParagraphArray(paraCursor);
if (paragraph != null) {
applyParagraphReplacements(paragraph, staticVars);
paraCursor++;
}
}
}
for (int i = 0; i < elements.size(); i++) {
doc.removeBodyElement(0);
}
}
private void handleLoopTable(XWPFTable table, Map<String, Object> model, Map<String, Object> staticVars) {
int markerRow = -1, markerCol = -1;
for (int r = 0; r < table.getRows().size(); r++) {
List<XWPFTableCell> cells = table.getRow(r).getTableCells();
for (int c = 0; c < cells.size(); c++) {
if (cells.get(c).getText().contains("##{loopRow")) {
markerRow = r;
markerCol = c;
break;
}
}
}
if (markerRow < 0) return;
String sourceKey = table.getRow(markerRow).getCell(markerCol + 1).getText();
List<Map<String, Object>> dynamicData = (List<Map<String, Object>>) model.get(sourceKey);
XWPFTable newTable = doc.createTable();
List<XWPFTableRow> templateRows = table.getRows();
for (int r = 0; r < markerRow; r++) {
XWPFTableRow newRow = newTable.createRow();
cloneRowStyle(newRow, templateRows.get(r));
replaceRowTags(newRow, staticVars);
}
if (dynamicData != null) {
XWPFTableRow templateDataRow = templateRows.get(markerRow + 1);
for (Map<String, Object> rowData : dynamicData) {
XWPFTableRow newRow = newTable.createRow();
cloneRowStyle(newRow, templateDataRow);
replaceRowTags(newRow, rowData);
}
}
for (int r = markerRow + 2; r < templateRows.size(); r++) {
XWPFTableRow newRow = newTable.createRow();
cloneRowStyle(newRow, templateRows.get(r));
replaceRowTags(newRow, staticVars);
}
newTable.removeRow(0);
doc.createParagraph();
}
private void applyTableReplacements(XWPFTable table, Map<String, Object> vars) {
XWPFTable newTable = doc.createTable();
for (XWPFTableRow row : table.getRows()) {
XWPFTableRow newRow = newTable.createRow();
cloneRowStyle(newRow, row);
}
newTable.removeRow(0);
replaceTableTags(newTable, vars);
doc.createParagraph();
}
private void cloneStaticTable(XWPFTable table) {
XWPFTable newTable = doc.createTable();
for (XWPFTableRow row : table.getRows()) {
XWPFTableRow newRow = newTable.createRow();
cloneRowStyle(newRow, row);
}
newTable.removeRow(0);
doc.createParagraph();
}
private void cloneRowStyle(XWPFTableRow target, XWPFTableRow source) {
while (target.getTableCells().size() < source.getTableCells().size()) {
target.addNewTableCell();
}
target.getCtRow().setTrPr(source.getCtRow().getTrPr());
for (int i = 0; i < target.getTableCells().size(); i++) {
copyCellContent(target.getCell(i), source.getCell(i));
}
}
private void copyCellContent(XWPFTableCell target, XWPFTableCell source) {
target.getCTTc().setTcPr(source.getCTTc().getTcPr());
for (int j = 0; j < target.getParagraphs().size(); j++) {
target.removeParagraph(j);
}
for (XWPFParagraph p : source.getParagraphs()) {
XWPFParagraph newP = target.addParagraph();
copyParagraphStyle(newP, p);
}
}
private void copyParagraphStyle(XWPFParagraph target, XWPFParagraph source) {
target.getCTP().setPPr(source.getCTP().getPPr());
for (int j = 0; j < target.getRuns().size(); j++) {
target.removeRun(j);
}
for (XWPFRun run : source.getRuns()) {
XWPFRun newRun = target.createRun();
newRun.getCTR().setRPr(run.getCTR().getRPr());
newRun.setText(run.text());
}
}
private void replaceRowTags(XWPFTableRow row, Map<String, Object> vars) {
for (XWPFTableCell cell : row.getTableCells()) {
for (XWPFParagraph p : cell.getParagraphs()) {
replaceTagsInParagraph(p, vars);
}
}
}
private void replaceTableTags(XWPFTable table, Map<String, Object> vars) {
for (XWPFTableRow row : table.getRows()) {
replaceRowTags(row, vars);
}
}
private void applyParagraphReplacements(XWPFParagraph templatePara, Map<String, Object> vars) {
XWPFParagraph newPara = doc.createParagraph();
newPara.getCTP().setPPr(templatePara.getCTP().getPPr());
for (XWPFRun run : templatePara.getRuns()) {
XWPFRun newRun = newPara.createRun();
newRun.getCTR().setRPr(run.getCTR().getRPr());
newRun.setText(run.text());
}
replaceTagsInParagraph(newPara, vars);
}
private void replaceTagsInParagraph(XWPFParagraph paragraph, Map<String, Object> vars) {
String fullText = paragraph.getText();
Pattern pattern = Pattern.compile("\\{.+?\\}");
Matcher matcher = pattern.matcher(fullText);
if (!matcher.find()) return;
int startRun = paragraph.searchText("{", new PositionInParagraph()).getBeginRun();
int endRun = paragraph.searchText("}", new PositionInParagraph()).getEndRun();
StringBuilder keyBuilder = new StringBuilder();
if (startRun == endRun) {
XWPFRun run = paragraph.getRuns().get(startRun);
String runText = run.text();
String key = runText.substring(runText.indexOf("{") + 1, runText.indexOf("}"));
String value = vars.getOrDefault(key, "").toString();
run.setText(value, 0);
} else {
XWPFRun startRunObj = paragraph.getRuns().get(startRun);
String startText = startRunObj.text();
keyBuilder.append(startText.substring(startText.indexOf("{") + 1));
for (int i = startRun + 1; i < endRun; i++) {
keyBuilder.append(paragraph.getRuns().get(i).text());
}
XWPFRun endRunObj = paragraph.getRuns().get(endRun);
String endText = endRunObj.text();
keyBuilder.append(endText.substring(0, endText.indexOf("}")));
String resolvedValue = vars.getOrDefault(keyBuilder.toString(), "").toString();
XWPFRun newRun = paragraph.insertNewRun(startRun);
newRun.setText(resolvedValue);
newRun.getCTR().setRPr(startRunObj.getCTR().getRPr());
for (int i = startRun + 1; i <= endRun; i++) {
paragraph.removeRun(startRun + 1);
}
}
}
public static void generateReport(String templatePath, String outputPath, Map<String, Object> data) throws Exception {
FileInputStream fis = new FileInputStream(templatePath);
DocxGenerator generator = new DocxGenerator(fis);
Map<String, Object> model = new java.util.HashMap<>();
model.put("staticVars", data);
generator.processTemplate(model);
FileOutputStream fos = new FileOutputStream(outputPath);
generator.writeToStream(fos);
fis.close();
fos.close();
}
}