Dans le cadre du développement d'applications d'entreprise, la génération de documents Word (contrats, rapports, factures) à partir de modèles prédéfinis est une fonctionnalité récurrente. L'utilisation directe de l'API Apache POI peut s'avérer complexe, notamment pour gérer les tableaux dynamiques ou l'insertion d'images sans briser la mise en forme.
Fonctionnalités principales de la solution
- Remplacement de texte simple : Support des variables de type
${nomUtilisateur}. - Insertion d'images dynamiqeus : Gestion des placeholders préfixés par
${@image_...}avec conservation du texte environnant. - Boucles sur tableaux multiples : Capacité à itérer sur plusieurs listes de données pour remplir différents tableaux dans un même document via le marqueur
${table:nomListe}. - Gestion intelligente des "Runs" : Fusion automatique des fragments de texte (Runs) pour assurer que les variables séparées par l'éditeur Word soient correctement identifiées.
- Formatage de date automatique : Conversion transparente des chaînes ISO (yyyy-MM-dd) vers un format lisible (ex: dd MMMM yyyy).
- Héritage de style : Les lignes générées dynamiquement conservent les bordures, les polices et l'alignement définis dans la ligne modèle.
Configuration Maven
Pour garantir la compatibilité, cette implémentation utilise les versions stables d'Apache POI (3.17 ou supérieures).
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>3.17</version>
</dependency>
Exemple d'utilisation
Voici comment préparer les données et appeler l'utilitaire pour générer un fichier final.
public class DemoExportWord {
public static void main(String[] args) {
try {
Map<String, Object> contexte = new HashMap<>();
// Données textuelles
contexte.put("clientNom", "Société Alpha");
contexte.put("dateContrat", "2024-05-20");
// Données d'image
contexte.put("@image_logo", "C:/ressources/logo.png");
// Données de tableau
List<Map<String, Object>> articles = new ArrayList<>();
Map<String, Object> art1 = new HashMap<>();
art1.put("designation", "Serveur Cloud");
art1.put("prix", "1500 €");
articles.add(art1);
contexte.put("listeArticles", articles);
ByteArrayOutputStream fluxSortie = WordProcessorUtils.processTemplate("modele_contrat.docx", contexte);
Files.write(Paths.get("contrat_final.docx"), fluxSortie.toByteArray());
} catch (Exception e) {
e.printStackTrace();
}
}
}
Implémentation de l'utilitaire central
Le code ci-dessous détaille la logique de traitement des paragraphes et des tableaux.
public class WordProcessorUtils {
private static final Pattern VAR_PATTERN = Pattern.compile("\\$\\{([^}]+)}");
private static final String IMAGE_MARKER = "@image_";
public static ByteArrayOutputStream processTemplate(String path, Map<String, Object> data) throws Exception {
try (InputStream is = new FileInputStream(path);
XWPFDocument document = new XWPFDocument(is)) {
// 1. Traitement des images
processImages(document, data);
// 2. Traitement des paragraphes simples
replaceInParagraphs(document.getParagraphs(), data);
// 3. Traitement des tableaux et boucles
processAllTables(document, data);
ByteArrayOutputStream out = new ByteArrayOutputStream();
document.write(out);
return out;
}
}
private static void processImages(XWPFDocument doc, Map<String, Object> data) throws Exception {
for (XWPFParagraph p : doc.getParagraphs()) {
handleImagePlaceholder(p, data);
}
for (XWPFTable tbl : doc.getTables()) {
for (XWPFTableRow row : tbl.getRows()) {
for (XWPFTableCell cell : row.getTableCells()) {
for (XWPFParagraph p : cell.getParagraphs()) {
handleImagePlaceholder(p, data);
}
}
}
}
}
private static void handleImagePlaceholder(XWPFParagraph p, Map<String, Object> data) throws Exception {
String text = getParagraphText(p);
for (Map.Entry<String, Object> entry : data.entrySet()) {
if (entry.getKey().startsWith(IMAGE_MARKER)) {
String placeholder = "${" + entry.getKey() + "}";
if (text.contains(placeholder)) {
String cleanText = text.replace(placeholder, "");
clearParagraphRuns(p);
p.createRun().setText(cleanText);
String imgPath = entry.getValue().toString();
insertPicture(p, imgPath);
}
}
}
}
private static void insertPicture(XWPFParagraph p, String path) throws Exception {
File imgFile = new File(path);
if (!imgFile.exists()) return;
try (FileInputStream is = new FileInputStream(imgFile)) {
XWPFRun run = p.createRun();
BufferedImage bimg = ImageIO.read(imgFile);
int width = 150; // Largeur fixe
int height = (int) (width * ((double) bimg.getHeight() / bimg.getWidth()));
run.addPicture(is, XWPFDocument.PICTURE_TYPE_PNG, imgFile.getName(),
Units.toEMU(width), Units.toEMU(height));
}
}
private static void processAllTables(XWPFDocument doc, Map<String, Object> data) {
for (XWPFTable table : doc.getTables()) {
String tableTag = findTableTag(table);
if (tableTag != null && data.get(tableTag) instanceof List) {
fillTableLogic(table, (List<Map<String, Object>>) data.get(tableTag), tableTag);
} else {
replaceInTableCells(table, data);
}
}
}
private static void fillTableLogic(XWPFTable table, List<Map<String, Object>> listData, String tag) {
XWPFTableRow modelRow = findModelRow(table);
if (modelRow == null) return;
int rowIndex = table.getRows().indexOf(modelRow);
for (Map<String, Object> rowData : listData) {
XWPFTableRow newRow = table.insertNewTableRow(++rowIndex);
copyRowStyle(modelRow, newRow);
fillRowData(newRow, rowData);
}
table.removeRow(table.getRows().indexOf(modelRow));
removeTableTag(table, tag);
}
private static void copyRowStyle(XWPFTableRow source, XWPFTableRow target) {
for (XWPFTableCell cell : source.getTableCells()) {
XWPFTableCell newCell = target.createCell();
newCell.getCTTc().setTcPr(cell.getCTTc().getTcPr());
}
}
private static void fillRowData(XWPFTableRow row, Map<String, Object> data) {
for (XWPFTableCell cell : row.getTableCells()) {
replaceInParagraphs(cell.getParagraphs(), data);
}
}
private static void replaceInParagraphs(List<XWPFParagraph> paragraphs, Map<String, Object> data) {
for (XWPFParagraph p : paragraphs) {
String fullText = getParagraphText(p);
if (fullText.contains("${")) {
String updatedText = performReplacement(fullText, data);
clearParagraphRuns(p);
p.createRun().setText(updatedText);
}
}
}
private static String performReplacement(String text, Map<String, Object> data) {
Matcher m = VAR_PATTERN.matcher(text);
StringBuffer sb = new StringBuffer();
while (m.find()) {
String key = m.group(1);
Object value = data.getOrDefault(key, "");
m.appendReplacement(sb, Matcher.quoteReplacement(value.toString()));
}
m.appendTail(sb);
return sb.toString();
}
private static String getParagraphText(XWPFParagraph p) {
StringBuilder res = new StringBuilder();
for (XWPFRun r : p.getRuns()) {
String t = r.getText(0);
if (t != null) res.append(t);
}
return res.toString();
}
private static void clearParagraphRuns(XWPFParagraph p) {
for (int i = p.getRuns().size() - 1; i >= 0; i--) {
p.removeRun(i);
}
}
private static String findTableTag(XWPFTable table) {
String text = table.getText();
Matcher m = Pattern.compile("\\$\\{table:([^}]+)}").matcher(text);
return m.find() ? m.group(1) : null;
}
private static XWPFTableRow findModelRow(XWPFTable table) {
for (XWPFTableRow row : table.getRows()) {
if (row.getText().contains("${") && !row.getText().contains("${table:")) {
return row;
}
}
return null;
}
private static void removeTableTag(XWPFTable table, String tag) {
String marker = "${table:" + tag + "}";
for (XWPFTableRow row : table.getRows()) {
for (XWPFTableCell cell : row.getTableCells()) {
for (XWPFParagraph p : cell.getParagraphs()) {
String text = getParagraphText(p);
if (text.contains(marker)) {
clearParagraphRuns(p);
p.createRun().setText(text.replace(marker, ""));
}
}
}
}
}
private static void replaceInTableCells(XWPFTable table, Map<String, Object> data) {
for (XWPFTableRow row : table.getRows()) {
fillRowData(row, data);
}
}
}
Points techniques à retenir
- Gestion des colonnes : Lors de la création dynamique de lignes, il est crucial de copier les propriétés XML (
TcPr) pour conserver les bordures et les espacements originaux du modèle. - Efficacité mémoire : L'utilisation de
ByteArrayOutputStreampermet de manipuler le docuemnt en mémoire sans créer de fichiers temporaires inutiles sur le serveur. - Traitement des Runs : Word segmente souvent le texte en fonction de la mise en forme ou des corrections orthographiques. Reconstruire la chaîne complète du paragraphe avant le remplacement est la seule méthode fiable pour ne pas manquer de variables.