Ao utilizar a biblioteca EasyPOI (versão 4.3.0) para gerar documentos Word a partir de templates, um bug conhecido ocorre quando a diretiva de iteração (loop) não está posicionada na primeira coluna de uma tabela. Nesses cenários, o processo de exportação falha em substituir o conteúdo dinâmico, resultendo em dados vazios no documento final.
Aálise da Causa Raiz
Ao depurar o fluxo de execução, o problema é isolado no método parseThisTable da classe ParseWord07. A implementação original assume incorretamente que a instrução de loop sempre residirá no índice zero (primeira coluna) da linha. Como o mecanismo de extração do objeto de lista ignora as demais células, templates com designs mais complexos quebram, pois o motor de renderização não consegue localizar o objeto de iteração (listobj).
Estratégia de Correção
Para contornar essa limitação sem aguardar um patch oficial, a abordagem mais ágil é sobrescrever as classes problemáticas diretamente no classpath do seu projeto. Isso envolve recriar a estrutura de pacotes original no seu código-fonte e substituir dois arquivos específicos: ParseWord07 e ExcelMapParse.
1. Atualizando a Classe ParseWord07
O objetivo aqui é iterar por todas as células da linha atual para localizar dinamicamente a coluna que contém a diretiva de loop, repassando esse índice para o processador. Localize o método parseThisTable (geralmente próximo à linha 199) e substitua a lógica de varredura pela versão otimizada abaixo:
/**
* Analisa a tabela atual para identificar e processar diretivas de iteração.
*
* @param table Instância da tabela Word
* @param dataMap Mapa contendo os dados para injeção
*/
private void parseThisTable(XWPFTable table, Map<String, Object> dataMap) throws Exception {
XWPFTableRow activeRow;
List<XWPFTableCell> rowCells;
Object collectionData;
for (int rowIndex = 0; rowIndex < table.getNumberOfRows(); rowIndex++) {
activeRow = table.getRow(rowIndex);
rowCells = activeRow.getTableCells();
collectionData = null;
int targetColIndex = 0;
// Varre todas as colunas para encontrar a diretiva de loop
for (XWPFTableCell cell : rowCells) {
collectionData = this.checkThisTableIsNeedIterator(cell, dataMap);
if (collectionData != null) {
break; // Diretiva encontrada, interrompe a busca na linha
}
targetColIndex++;
}
// Processamento baseado no tipo de dado encontrado
if (collectionData == null) {
this.parseThisRow(rowCells, dataMap);
} else if (collectionData instanceof ExcelListEntity) {
ExcelListEntity listEntity = (ExcelListEntity) collectionData;
(new ExcelEntityParse()).parseNextRowAndAddRow(table, rowIndex, listEntity);
// Avança o índice de acordo com o tamanho da lista processada
rowIndex += listEntity.getList().size() - 1;
} else {
List<?> dataList = (List<?>) collectionData;
ExcelMapParse.parseNextRowAndAddRow(table, rowIndex, dataList, targetColIndex);
rowIndex += dataList.size() - 1;
}
}
}
2. Atualizando a Classe ExcelMapParse
Em seguida, o método parseNextRowAndAddRow (linha 144) deve ser adaptado para aceitar e utilizar o índice da coluna descoberto no passo anterior. Esta revisão também corrige falhas ao lidar com células mescladas e garante a limpeza adequada do texto residual nas células. Substitua a implementação existente pelo código reestruturado a seguir:
/**
* Expande a tabela duplicando linhas com base na coleção de dados,
* começando a análise a partir da coluna específica.
*
* @param table Tabela alvo
* @param startIndex Índice da linha inicial
* @param dataList Lista de objetos a serem renderizados
* @param directiveCol Índice da coluna que contém a tag de loop
*/
public static void parseNextRowAndAddRow(XWPFTable table, int startIndex, List<Object> dataList, int directiveCol) throws Exception {
XWPFTableRow baseRow = table.getRow(startIndex);
String[] cellExpressions = parseCurrentRowGetParams(baseRow);
String loopDirective = cellExpressions[directiveCol];
boolean requiresRowCreation = !loopDirective.contains("!fe:");
// Limpeza da string de diretiva
loopDirective = loopDirective.replace("!fe:", "")
.replace("$fe:", "")
.replace("fe:", "")
.replace("{{", "");
String[] parsedKeys = loopDirective.replaceAll("\\s{1,}", " ").trim().split(" ");
cellExpressions[directiveCol] = parsedKeys[1];
List<XWPFTableCell> originalCells = new ArrayList<>(table.getRow(startIndex).getTableCells());
Map<String, Object> evalContext = Maps.newHashMap();
LOGGER.debug("Iniciando iteração para lista de dados com tamanho: {}", dataList.size());
int currentRowIndex = startIndex;
for (Object item : dataList) {
XWPFTableRow activeRow = requiresRowCreation ? table.insertNewTableRow(currentRowIndex++) : table.getRow(currentRowIndex++);
evalContext.put("t", item);
// Ajuste de segurança para células mescladas que causam desalinhamento no array
String[] safeExpressions = ArrayUtils.clone(cellExpressions);
if (cellExpressions.length < activeRow.getTableCells().size()) {
int missingCells = activeRow.getTableCells().size() - cellExpressions.length;
for (int i = 0; i < missingCells; i++) {
safeExpressions = ArrayUtils.add(safeExpressions, 0, "empty_placeholder_" + i);
}
}
int cellIdx;
// Preenche as células existentes na linha
for (cellIdx = 0; cellIdx < activeRow.getTableCells().size(); ++cellIdx) {
String evaluatedValue = PoiElUtil.eval(safeExpressions[cellIdx], evalContext).toString();
// Correção para remoção efetiva de conteúdo de células (o método setText("") original falha)
if (!Strings.isNullOrEmpty(evaluatedValue)) {
activeRow.getTableCell(cellIdx).getParagraphs()
.forEach(paragraph -> paragraph.getRuns().forEach(run -> run.setText("", 0)));
}
XWPFTableCell sourceCell = cellIdx >= originalCells.size() ? originalCells.get(originalCells.size() - 1) : originalCells.get(cellIdx);
PoiWordStyleUtil.copyCellAndSetValue(sourceCell, activeRow.getTableCell(cellIdx), evaluatedValue);
}
// Cria e preenche células adicionais se as expressões excederem as células originais
while (cellIdx < safeExpressions.length) {
String evaluatedValue = PoiElUtil.eval(safeExpressions[cellIdx], evalContext).toString();
PoiWordStyleUtil.copyCellAndSetValue(originalCells.get(cellIdx), activeRow.createCell(), evaluatedValue);
++cellIdx;
}
}
// Remove a linha de template original ao final da iteração
table.removeRow(currentRowIndex);
}