Generating Word Documents from Templates Using Apache POI

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

Tags: Apache POI DocX java Word template Document Generation

Posted on Wed, 21 Oct 2026 16:03:14 +0000 by ou812