Tag: apache poi

Entenda definitivamente como usar o Apache POI: user model, shiftRows e clonagem de célula

Trabalhar com arquivos Excel é uma tarefa comum em empresas que fornecem software que gerenciam dados e valores de negócio. Mas nem sempre é fácil.

Há algum tempo publiquei um artigo sobre como adicionar segurança em arquivos do Excel com o POI. Também estou trabalhando num pequeno framework para gerar arquivos Excel com base em templates baseado no MVEL e no POI.

Deu pra notar que estou mexendo bastante com isso, né? Vamos a algumas dicas para quem está começando com o Apache POI.

User models – o que são e como usar

A primeira dica é algo que você deve saber. A biblioteca POI possui três diferentes APIs para trabalhar com arquivos Excel:

  1. HSSF: classes para trabalhar com o formado binário mais antigo do Excel (XLS).
  2. XSSF: classes para trabalhar com o formato mais novo do Excel baseado em XML (XLSX).
  3. SS: supermodelo que engloba tanto HSSF quanto XSSF.

Cada uma dessas APIs representa um user model, isto é, um modelo de dados para que o código “cliente” (o seu código) possa manipular um tipo específico de arquivo.

Ao trabalhar com classes do pacote hssf, você somente poderá ler e criar arquivos XLS. O pacote xssf somente lê e cria arquivos XLSX.

Enfim, use sempre as classes do pacote ss (supermodel) pois elas servem para os dois tipos de arquivos do Office. Caso contrário, você terá que fazer implementações distintas para trabalhar com arquivos binários e XML.

Desmistificando o método shiftRows

Uma das funcionalidades que implementei no desenvolvimento do framework foi a replicação automática de linhas. Também implementei um operador condicional para incluir ou não linhas no arquivo de acordo com variáveis. Para tanto, foi necessário implementar código para incluir e remover intervalores de linhas do arquivo.

Estudando a API, encontrei a função shiftRows da classe Sheet. Este método é frequentemente mal usado pelos programadores que não entendem bem como ele funciona.

Ok, a ideia do termo shift seria para deslocar as linhas. Mas o método possui os três parâmetros:

void shiftRows(int startRow, int endRow, int n)

Em geral, o entendimento inicial que as pessoas tem é que esse comando seria similar à funcionalidade do Excel de replicar linhas. Por exemplo, você seleciona as linhas de 1 a 3 e replica n vezes. Considere o comando:

sheet.shiftRows(0, 2, 5);

Será que esse comando copia as três primeiras linhas da planilha cinco vezes? A resposta é não.

Na verdade, o método shiftRows apenas desloca as linhas, alterando o número das linhas selecionadas. O comando acima simplesmente incrementa em 5 o número da linha das três primeiras linhas da planilha.

Após a chamada ao método, a linha 0 (primeira linha) irá para a posição 5 (sexta linha), a linha 1 irá para a posição 6 (sétima linha) e a linha 2 irá para a posição 7 (oitava linha).

Confuso? Pense da seguinte forma:

Pegue as linhas de 0 a 2 e mova cinco posições para “baixo”.

O mesmo raciocínio poderia ser aplicado caso o terceiro parâmetro fosse negativo. No caso, isso significa mover as linhas para “cima”. Enfim, este parâmetro diz em quanto as linhas selecionadas devem ser deslocadas, para “baixo” ou para “cima”.

Problemas com o shiftRows

É preciso tomar cuidado ao usar o método para não acabar com uma planilha corrompida. Isso já ocorreu várias vezes com várias pessoas que conheço.

O problema é que, ao deslocar as linhas com shiftRows, elas podem sobrepor outras linhas que já existem naquela posição. E isso pode causar vários comportamentos estranhos, desde a perda de alguns dados até uma planilha totalmente corrompida.

Imagine, no exemplo anterior, se já existissem as linhas 5, 6 ou 7.

Aparentemente, o POI não tem um tratamento para isso. Sinceramente, não verifiquei se isso foi corrigido em versões recentes, mas não é raro lidarmos com versões anteriores em projetos já existentes.

Portanto, sempre que for incluir linhas, mova todas as linhas até o final da planilha.

Exemplo:

sheet.shiftRows(
    posicaoParaInserir, 
    sheet.getLastRowNum(), 
    quantidadeLinhasNovas);

Para remover linhas, primeiramente é preciso remover manualmente os objetos do tipo Row existentes no intervalo, depois deslocar todas as linhas de baixo para cima.

Veja como ficou uma implementação minha para remover um intervalo de linhas:

private void removeRows(Sheet sheet, int position, int count) {
    for (int i = 0; i < count; i++) {
        Row row = sheet.getRow(position + i);
        if (row != null) {
            sheet.removeRow(row);
        }

    }
    sheet.shiftRows(position + count, sheet.getLastRowNum(), -count);
}

Explicando novamente, o código acima faz o seguinte:

  1. Remove as linhas existentes no intervalo começando em position até a quantidade count. Note que é necessário verificar se o objeto Row existe para cada linha, porque a implementação é de uma lista esparsa, isto é, as linhas que não tem informação não existem na memória.

  2. Desloca as linhas que estão acima do intervalo para cima, aplicando um valor negativo ao tamanho do intervalo. Em outras palavras, se eu quiser excluir as linhas 6 a 10, todas as linhas a partir da linha 11 terão o seu número subtraído de 5.

Clonar células

No Excel é simples duplicar o conteúdo com formatação. No POI esta tarefa demanda um certo trabalho. Não há um comando pronto, sendo necessário copiar o conteúdo, o estilo e alguns outros atributos de cada célula.

Fiz uma implementação baseada neste artigo do Stack Overflow:

private void cloneRow(Row sourceRow, Row newRow) {

    // Loop through source columns to add to new row
    for (Cell oldCell : sourceRow) {

        // Grab a copy of the old/new cell
        Cell newCell = newRow.createCell(oldCell.getColumnIndex());

        // Copy style from old cell and apply to new cell
        CellStyle newCellStyle = workbook.createCellStyle();
        newCellStyle.cloneStyleFrom(oldCell.getCellStyle());
        newCell.setCellStyle(newCellStyle);

        // If there is a cell comment, copy
        if (oldCell.getCellComment() != null) {
            newCell.setCellComment(oldCell.getCellComment());
        }

        // If there is a cell hyperlink, copy
        if (oldCell.getHyperlink() != null) {
            newCell.setHyperlink(oldCell.getHyperlink());
        }

        // Set the cell data type
        newCell.setCellType(oldCell.getCellType());

        // Set the cell data value
        switch (oldCell.getCellType()) {
            case Cell.CELL_TYPE_BLANK:
                newCell.setCellValue(oldCell.getStringCellValue());
                break;
            case Cell.CELL_TYPE_BOOLEAN:
                newCell.setCellValue(oldCell.getBooleanCellValue());
                break;
            case Cell.CELL_TYPE_ERROR:
                newCell.setCellErrorValue(oldCell.getErrorCellValue());
                break;
            case Cell.CELL_TYPE_FORMULA:
                newCell.setCellFormula(oldCell.getCellFormula());
                break;
            case Cell.CELL_TYPE_NUMERIC:
                newCell.setCellValue(oldCell.getNumericCellValue());
                break;
            case Cell.CELL_TYPE_STRING:
                newCell.setCellValue(oldCell.getRichStringCellValue());
                break;
        }
    }

    // If there are are any merged regions in the source row, copy to new row
    Sheet sheet = sourceRow.getSheet();
    for (int i = 0; i < sheet.getNumMergedRegions(); i++) {
        CellRangeAddress cellRangeAddress = sheet.getMergedRegion(i);
        if (cellRangeAddress.getFirstRow() == sourceRow.getRowNum()) {
            CellRangeAddress newCellRangeAddress = new CellRangeAddress(
                    newRow.getRowNum(),
                    (newRow.getRowNum() +
                            (cellRangeAddress.getLastRow() - cellRangeAddress.getFirstRow())),
                    cellRangeAddress.getFirstColumn(),
                    cellRangeAddress.getLastColumn());
            sheet.addMergedRegion(newCellRangeAddress);
        }
    }

}

Considerações finais

A biblioteca POI é bem flexível e permite manipular arquivos de planilha de forma relativamente simples e eficiente.

Porém, determinadas funcionalidades podem representar um desafio até para o entendimento dela. Espero que este artigo, dentro dos cenários propostos, lhe possa ser útil. 😀

Apache POI: adicionando segurança em arquivos Excel

poi-original
A biblioteca Apache POI possibilita a leitura e gravação de documentos do Microsoft Office via código Java.

Por esses dias, fiz uma pesquisa sobre como essa biblioteca viabiliza (ou não) trabalharmos com arquivos protegidos do Excel.

Níveis de segurança do Office

Documentos do Excel podem ter diferentes níveis de proteção, a saber:

  • Proteção contra alteração: permite abrir o documento em modo somente-leitura, mas não deixa o usuário alterá-lo sem antes digitar a senha.
  • Proteção contra alteração de um trecho do documento: protege planilhas e células específicas com senha.
  • Proteção contra leitura: não permite abrir o documento sem a senha.

Todos esses modos podem ser usados ao mesmo tempo, inclusive com senhas diferentes.

Se o arquivo tiver proteção contra leitura, o Excel irá solicitar a senha antes de abrir o mesmo. Se houver proteção contra alteração ele irá solicitar a senha para desbloqueio da edição ou permitir acesso somente-leitura ao documento. Se planilhas ou células estiverem protegidas contra alteração, então o usuário deve acessar a função “Desproteger” no Excel e digitar a senha de desbloqueio para cada trecho.

As proteções contra alteração são simples travas no editor, pois o arquivo em si permanece aberto e não há restrições de alteração via programação ou com o uso de editores de terceiros. Podemos dizer que este mecanismo oferece um baixíssimo grau de segurança, que impedirá apenas o usuário mais leigo de efetivamente modificar o documento.

Por outro lado, a proteção contra leitura oferece um grau de segurança maior, pois o documento como um todo é criptografado. Isso significa que nenhum programa ou editor conseguirá sequer ler o documento sem antes descriptografar os bytes do arquivo com a senha original.

Versões dos documentos do Office

Quem acompanha o Office há algum tempo deve ter notado que, a partir da versão 2007, os documentos ganharam novas extensões, com o sufixo x. Por exemplo, doc passou a ser docx, xls mudou para xlsx e assim por diante.

Mas não foi apenas a extensão que mudou. O conteúdo foi completamente reformulado. O formato mais antigo era do tipo binário, enquanto os novos são baseados em XML.

As vantagens do formato XML são inúmeras, a começar pela capacidade de qualquer ferramenta que trabalha com XML conseguir processar o documento ou pelo menos parte dele.

No que se refere à biblioteca Apache POI, isso também nos afeta diretamente, pois há APIs diferentes para trabalhar com os diferentes formatos de documentos.

Como veremos adiante, nem todos os formatos suportam todos os tipos de segurança.

Capacidades do POI

A conclusão dos meus testes foi a seguinte:

Tipo de documento Operação Proteção contra alteração do documento Proteção contra alteração de uma planilha Proteção contra Leitura (criptografia)
XLS
Leitura OK
Criação N/D* OK N/D
XLSX
Leitura OK
Criação N/D OK OK

Na leitura de um arquivo XLS ou XLSX, as proteções contra alteração são ignoradas, então não há o que testar.
N/D = Não disponível, isto é, não há suporte na API.
* Existe um método para proteger o documento, mas ele não surte efeito (bug).

Como pode ser visto, para documentos no formato legado XLS o POI suporta apenas a leitura de arquivos criptografados. Na criação, ele apenas consegue proteger parte do documento contra alteração.

Já para o formato mais novo XLSX, o POI consegue ler e criar arquivos criptografados. Entretanto, na criação do documento, não há um método para proteger o documento todo contra alteração.

Alguns fóruns sugerem uma possibilidade de contornar a falta do recurso de proteger o documento como um todo contra alteração. Para isso, basta criar um arquivo Excel com tal proteção e usá-lo como template para geração de um novo.

Implementação

Criptografia em arquivos XLS

O POI suporta apenas ler arquivos criptografados neste formato, isto é, que estão protegidos contra leitura.

O segredo é a classe Biff8EncryptionKey. Basta definir a senha através do método estático setCurrentUserPassword e depois ler o arquivo normalmente.

Exemplo:

try {
    Biff8EncryptionKey.setCurrentUserPassword(password);
    new HSSFWorkbook(input);
} finally {
    Biff8EncryptionKey.setCurrentUserPassword(null);
}

É bom não esquecer de remover a senha ao final para não atrapalhar futuras leituras.

Mesmo sendo um método estático, não deve haver problemas de concorrências, pois a documentação afirma que a senha é armazenada usando ThreadLocal.

Criptografia em arquivos XLSX

O POI implementa tanto a leitura quanto a criação de documentos criptografados neste formato.

Veja minha implementação para a leitura:

public class XlsxDecryptor {

    public static XSSFWorkbook decrypt(InputStream input, String password)
            throws IOException {

        POIFSFileSystem filesystem = new POIFSFileSystem(input);
        EncryptionInfo info = new EncryptionInfo(filesystem);
        Decryptor d = Decryptor.getInstance(info);

        try {
            if (!d.verifyPassword(password)) {
                throw new RuntimeException("Unable to process: document is encrypted");
            }

            InputStream dataStream = d.getDataStream(filesystem);
            return new XSSFWorkbook(dataStream);
        } catch (GeneralSecurityException ex) {
            throw new RuntimeException("Unable to process encrypted document",
                ex);
        }

    }

    private XlsxDecryptor() {
    }

}

E para criação:

public class XlsxEncryptor {

    public static void encrypt(InputStream input, OutputStream output, 
            String password) throws IOException {

        try {
            POIFSFileSystem fs = new POIFSFileSystem();
            EncryptionInfo info = new EncryptionInfo(fs, EncryptionMode.agile);

            Encryptor enc = info.getEncryptor();
            enc.confirmPassword(password);

            OPCPackage opc = OPCPackage.open(input);
            OutputStream os = enc.getDataStream(fs);
            opc.save(os);
            opc.close();

            fs.writeFilesystem(output);
            output.close();
        } catch (GeneralSecurityException e) {
            throw new RuntimeException(e);
        } catch (InvalidFormatException e) {
            throw new RuntimeException(e);
        }

    }

}

Proteção de partes de um documento

O POI implementa a proteção de uma planilha de um documento Excel e a proteção de células. Testei apenas a proteção de planilhas.

A implementação é muito simples, basta usar o método protectSheet. Veja o exemplo para o formato XLSX:

XSSFSheet sheet = workbook.createSheet();
sheet.protectSheet("54321");

E agora o equivalente para o formato XLS:

HSSFSheet sheet = workbook.createSheet();
sheet.protectSheet("54321");

Testes e exemplo

Antes de encerrar o artigo, vou acrescentar aqui dois dos testes unitários que implementei de forma que possam servir como exemplo de uso do POI e do meu projeto.

O primeiro cria um novo arquivo XLSX cripgrafado e em seguida lê o mesmo arquivo verificando se o valor foi gravado corretamente:

@Test
public void createXLSXOpenAndModifyProtected() throws IOException {
    System.out.println("createXLSXOpenAndModifyProtected");

    //creates sheet
    XSSFWorkbook workbook = new XSSFWorkbook();
    XSSFSheet sheet = workbook.createSheet();
    sheet.protectSheet("54321");

    XSSFRow row = sheet.createRow(0);
    XSSFCell cell = row.createCell(0);
    cell.setCellValue("Gravado");

    //saves sheet
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    workbook.write(bos);
    bos.close();
    ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());

    new File("target/.output-file/xlsx").mkdirs();
    WorksheetEncryptor.encrypt(
        bis, 
        new FileOutputStream("target/.output-file/xlsx/OpenAndModifyProtected.xlsx"), 
        DocumentType.XML, 
        "54321");
    bis.close();

    //read again and check
    XSSFWorkbook workbook2 = (XSSFWorkbook) WorksheetDecryptor.decrypt(
        new FileInputStream("target/.output-file/xlsx/OpenProtected.xlsx"),
        DocumentType.XML, 
        "54321");
    Assert.assertEquals("Gravado", workbook2.getSheetAt(0).getRow(0).getCell(0).getStringCellValue());
}

Em seguida, o segundo lê um arquivo XLS criptografado e verifica o valor da célula:

@Test
public void testOpenAndModifyProtectedXLS() throws IOException {
    System.out.println("### testOpenAndModifyProtectedXLS");
    HSSFWorkbook workbook = (HSSFWorkbook) WorksheetDecryptor.decrypt(
        getClass().getResourceAsStream("xls/OpenAndModifyProtected.xls"),
        DocumentType.LEGACY, 
        "12345");
    String conteudoCelula = workbook.getSheetAt(0).getRow(0).getCell(0)
        .getStringCellValue();
    Assert.assertEquals("Teste criptografia", conteudoCelula);
}

Código-fonte

Todo o código, inclusive os testes, está disponível em um projeto no meu GitHub.

Com o Maven você será capaz de executá-lo sem nenhuma configuração adicional. Basta clonar o repositório.

Creative Commons O blog State of the Art de Luiz Ricardo é licenciado sob uma Licença Creative Commons. Copie, compartihe e modifique, apenas cite a fonte.