Resolvendo o Problema de Exportação Vazia no EasyPOI 4.3.0 ao Usar Loops Fora da Primeira Coluna em Templates Word

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

Tags: easypoi apache-poi word-export java template-engine

Publicado em 9-5 06:09