Categorias

Compressão e Descompressão de dados em Java

Neste artigo veremos como trabalhar com arquivos compactados em aplicações Java. Vamos conhecer o pacote java.util.zip, presente na plataforma Java SE desde a versão 1.1, que oferece recursos para a criação e manipulação de arquivos no formato Zip e Gzip, além também do formato Jar, que é, basicamente, um arquivo Zip que contem classes Java e outros recursos dentro dele.

O artigo foi baseado no Java 5 (versão 1.5), mas deve funcionar com a versão 1.4.2.

Visão geral da API

É no pacote java.util.zip que se encontram todas as classes da API padrão do Java para ler e criar arquivos compactados.

O pacote possui ao todo um conjunto de 14 classes, 2 exceções e 1 interface. Deste conjunto, vamos apenas nos concentrar nas classes mais importantes e necessárias para o objetivo do artigo, que é a criação e a extração de arquivos compactados, portanto, vamos conhecer e estudar as classes ZipFile, ZipInputStream, ZipOutputStream e ZipEntry.

As duas primeiras classes são as responsáveis por lerem e extrairem o conteúdo dos arquivos Zip, no entanto, elas não são usadas em conjunto, mas sim separadamente. Ambas têm a mesma função, porém com características diferentes. A classe seguinte, ZipOutputStream, é responsável por criar e gravar o Zip e a última representa uma entrada do Zip.

Todo arquivo Zip é composto de uma ou mais entradas. Cada entrada corresponde a um arquivo ou diretório que originalmente foi compactado e armazenado no Zip e é representado pela classe ZipEntry. Esta classe possui métodos que permitem recuperar informações de cada entrada armazenada no Zip. Veja a Tabela 1:

Tabela 1. Métodos para recuperar informações da classe ZipEntry

MÉTODO RETORNO

  • getName() – Nome da entrada no zip.
  • getCompressedSize() – Tamanho do dado compactado da entrada ou -1 se desconhecido.
  • getSize() – Tamanho original da entrada ou -1 se desconhecido.
  • getTime() – Data e hora de modificação da entrada.
  • isDirectory() – Booleano que indica se é um diretório.
  • getMethod() – Método de compressão usado para armazenar a entrada. Pode ser comparado com as constantes STORED e DEFLATED da própria classe.

Mais características das classes mencionadas e o modo de se recuperar um entrada do Zip serão apresentadas ao longo do artigo.

Para facilitar o entendimento do artigo e simplificar o uso de termos, chamaremos os arquivos Zip apenas de Zip.

Listando o conteúdo do Zip

Ler e descompactar o conteúdo de um Zip é apenas uma questão de abrir um stream para o arquivo e ler os dados retornados, da mesma maneira que se lê arquivos comuns, não compactados.

As classes ZipFile e ZipInputStream oferecem facilidades de leitura e extração de arquivos Zip, sendo possível recuperar cada um dos arquivos ou diretórios armazenados. Como já mencionado, elas não trabalham em conjunto, mas sim separadamente, pois ambas desempenham a mesma funcionalidade, porém com características diferentes.

A classe ZipFile dispensa o uso explícito de streams para abrir o Zip e mantém um cache interno das entradas, assim, caso o Zip necessite ser aberto novamente, a operação será mais rápida que no primeiro acesso. Entretanto o uso de ZipFile não é aconselhável caso o Zip seja alterado constantemente por outras aplicações, pois o cache de entradas pode ficar desatualizado e causar inconsistência. Já a classe ZipInputStream usa streams para abrir o Zip e não mantém um cache das entradas.

O exemplo da Listagem 1 abre um Zip existente em disco usando a classe ZipFile, e exibe na saída padrão o nome de todas as entradas (arquivos e diretórios) armazenadas nele. O Zip a ser aberto é determinado ao instanciar ZipFile, cujo construtor pode receber um string com o caminho do Zip, ou um java.io.File.

Listagem 1: ListagemZip.java

01 package jm.zipper;
02 import java.io.*;
03 import java.util.zip.*;
04 
05 public class ListagemZip {
06   public static void main(String[] args) throws IOException {
07     String arquivo = "c:/teste.zip";
08     ZipFile zip = new ZipFile( arquivo );
09     Enumeration e = zip.entries();
10     while( e.hasMoreElements() ) {
11       ZipEntry entrada = (ZipEntry) e.nextElement();
12       System.out.println( entrada.getName() );
13     }
14     zip.close();
15   }
16 }

As entradas do Zip são recuperadas com o método entries() de ZipFile, que retorna um java.util.Enumeration permitindo navegar por uma coleção de objetos ZipEntry.

1 ZipFile zip = new ZipFile("c:/teste.zip");
2 Enumeration entradas = zip.entries();

Com a atualização da API para o Java 5, foi adotado o uso de Generics no retorno deste método, que na verdade retorna um java.util.Enumeration, ou seja, ele define que o retorno é um Enumeration com objetos ZipEntry ou seus subtipos. Este é o único caso de utilização de Generics na API, portanto, todo o resto segue o padrão de utilização do Java 1.4. Então, quem usa Java 5 pode fazer desta forma:

1 Enumeration entradas = zip.entries();

As entradas são recuperadas uma a uma, a cada iteração do laço while, com nextElement(), e o nome da entrada é obtido com getName() e exibido na saída padrão:

1 while(entradas.hasMoreElements()) {
2    ZipEntry entrada = (ZipEntry) entradas.nextElement();
3    System.out.println(entrada.getName());
4 }

A outra forma de trabalhar com o Zip, usando a classe ZipInputStream, está demonstrada na Listagem 2, que faz o mesmo que o primeiro exemplo – abre o Zip e lista as entradas na saída padrão.

Listagem 2: ListagemZip2.java

01 package jm.zipper;
02 import java.io.*;
03 import java.util.zip.*;
04 
05 public class ListagemZip2 {
06   public static void main(String[] args) throws IOException {
07     String arquivo = "c:/teste.zip";
08     FileInputStream fis = new FileInputStream( arquivo );
09     ZipInputStream zis = new ZipInputStream( fis );
10     ZipEntry entrada = null;
11     while( (entrada = zis.getNextEntry()) != null ) {
12       System.out.println( entrada.getName() );
13     }
14     zis.close();
15     fis.close();
16   }
17 }

Inicialmente, abrimos um FileInputStream para o Zip. Em seguida criamos um objeto ZipInputStream, que recebe um InputStream em seu construtor. É este objeto que lê o Zip e devolve os dados nele armazenados.

O método getNextEntry() retorna um objeto do tipo ZipEntry, que representa uma entrada (arquivo ou diretório) armazenada no Zip. Como no primeiro exemplo, a cada iteração do while, é obtida a próxima entrada do Zip e exibido o seu nome na saída padrão.

A aplicação de exemplo

Para exemplificar a extração e a criação de arquivos Zip mais completamente, criamos uma aplicação gráfica, no estilo do aplicativo Winzip, porém mais simples, mostrada em execução na Figura 1.

Figura 1: Aplicação de exemplo listando o conteúdo de um Zip.

A aplicação usa uma classe principal, com métodos que encapsulam as funcionalidades de manipulação de Zips, que criamos para simplificar as operações e também poder ser reusada em outros casos e aplicações. É esta classe que descreveremos aqui. O restante das classes não será apresentado, mas está disponível para download.

A Listagem 3 exibe o código da classe jm.Zipper. No programa de exemplo foram usados os recursos de ZipFile para a extração do Zip, mas mostraremos paralelamente como utilizar ZipInputStream para o mesmo fim.

001 package jm.zipper;
002 import java.io.*;
003 import java.util.*;
004 import java.util.zip.*;
005 
006 public class Zipper {
007   public List listarEntradasZip( File arquivo ) throws ZipException, IOException {
008     List entradasDoZip = new ArrayList();
009     ZipFile zip = null;
010     try {
011       zip = new ZipFile( arquivo );
012       Enumeration e = zip.entries();
013       ZipEntry entrada = null;
014       while( e.hasMoreElements() ) {
015         entrada = (ZipEntry) e.nextElement();
016         entradasDoZip.add ( entrada );
017       }
018       setArquivoZipAtual( arquivo );
019     }
020     finally {
021       if( zip != null ) {
022         zip.close();
023       }
024     }
025     return entradasDoZip;
026   }
027 
028   public void extrairZip( File diretorio ) throws ZipException, IOException {
029     extrairZip( this.getArquivoZipAtual(), diretorio );
030   }
031 
032   public void extrairZip( File arquivoZip, File diretorio ) throws ZipException, IOException {
033     ZipFile zip = null;
034     File arquivo = null;
035     InputStream is = null;
036     OutputStream os = null;
037     byte[] buffer = new byte[TAMANHO_BUFFER];
038     try {
039       //cria diretório informado, caso não exista
040       if( !diretorio.exists() ) {
041         diretorio.mkdirs();
042       }
043       if( !diretorio.exists() || !diretorio.isDirectory() ) {
044         throw new IOException("Informe um diretório válido");
045       }
046       zip = new ZipFile( arquivoZip );
047       Enumeration e = zip.entries();
048       while( e.hasMoreElements() ) {
049         ZipEntry entrada = (ZipEntry) e.nextElement();
050         arquivo = new File( diretorio, entrada.getName() );
051         //se for diretório inexistente, cria a estrutura 
052         //e pula pra próxima entrada
053         if( entrada.isDirectory() && !arquivo.exists() ) {
054           arquivo.mkdirs();
055           continue;
056         }
057         //se a estrutura de diretórios não existe, cria
058         if( !arquivo.getParentFile().exists() ) {
059           arquivo.getParentFile().mkdirs();
060         }
061         try {
062           //lê o arquivo do zip e grava em disco
063           is = zip.getInputStream( entrada );
064           os = new FileOutputStream( arquivo );
065           int bytesLidos = 0;
066           if( is == null ) {
067             throw new ZipException("Erro ao ler a entrada do zip: "+entrada.getName());
068           }
069           while( (bytesLidos = is.read( buffer )) > 0 ) {
070             os.write( buffer, 0, bytesLidos );
071           }
072         } finally {
073           if( is != null ) {
074             try {
075               is.close();
076             } catch( Exception ex ) {}
077           }
078           if( os != null ) {
079             try {
080               os.close();
081             } catch( Exception ex ) {}
082           }
083         }
084       }
085     } finally {
086       if( zip != null ) {
087         try {
088           zip.close();
089         } catch( Exception e ) {}
090       }
091     }
092   }
093   
094   public List criarZip( File arquivoZip, File[] arquivos ) throws ZipException, IOException {
095     FileOutputStream fos = null;
096     BufferedOutputStream bos = null;
097     setArquivoZipAtual( null );
098     try {
099       //adiciona a extensão .zip no arquivo, caso não exista
100       if( !arquivoZip.getName().toLowerCase().endsWith(".zip") ) {
101         arquivoZip = new File( arquivoZip.getAbsolutePath()+".zip" );
102       }
103       fos = new FileOutputStream( arquivoZip );
104       bos = new BufferedOutputStream( fos, TAMANHO_BUFFER );
105       List listaEntradasZip = criarZip( bos, arquivos );
106       setArquivoZipAtual( arquivoZip );
107       return listaEntradasZip;
108     }
109     finally {
110       if( bos != null ) {
111         try {
112           bos.close();
113         } catch( Exception e ) {}
114       }
115       if( fos != null ) {
116         try {
117           fos.close();
118         } catch( Exception e ) {}
119       }
120     }
121   }
122   
123   public List criarZip( OutputStream os, File[] arquivos ) throws ZipException, IOException {
124     if( arquivos == null || arquivos.length < 1 ) {
125       throw new ZipException("Adicione ao menos um arquivo ou diretório");
126     }
127     List listaEntradasZip = new ArrayList();
128     ZipOutputStream zos = null;
129     try {
130       zos = new ZipOutputStream( os );
131       for( int i=0; i= 0 ) {
170         //calcula os diretórios a partir do diretório inicial
171         //isso serve para não colocar uma entrada com o caminho completo
172         caminhoEntradaZip = arquivo.getAbsolutePath().substring( idx+caminhoInicial.length()+1 );
173       }
174       ZipEntry entrada = new ZipEntry( caminhoEntradaZip );
175       zos.putNextEntry( entrada );
176       zos.setMethod( ZipOutputStream.DEFLATED );
177       fis = new FileInputStream( arquivo );
178       bis = new BufferedInputStream( fis, TAMANHO_BUFFER );
179       int bytesLidos = 0;
180       while((bytesLidos = bis.read(buffer, 0, TAMANHO_BUFFER)) != -1) {
181         zos.write( buffer, 0, bytesLidos );
182       }
183       listaEntradasZip.add( entrada );
184     }
185     finally {
186       if( bis != null ) {
187         try {
188           bis.close();
189         } catch( Exception e ) {}
190       }
191       if( fis != null ) {
192         try {
193           fis.close();
194         } catch( Exception e ) {}
195       }
196     }
197     return listaEntradasZip;
198   }
199   
200   public void fecharZip() {
201     setArquivoZipAtual( null );
202   }
203   
204   public File getArquivoZipAtual() {
205     return arquivoZipAtual;
206   }
207   
208   private void setArquivoZipAtual(File arquivoZipAtual) {
209     this.arquivoZipAtual = arquivoZipAtual;
210   }
211   
212   private File arquivoZipAtual;
213   private static final int TAMANHO_BUFFER = 2048; // 2 Kb
214 }

A Tabela 2 resume os métodos públicos dessa classe.

Tabela 2. Métodos públicos da classe de exemplo Zipper

MÉTODO DESCRIÇÃO

  • listarEntradasZip( File ) – Abre o arquivo zip informado e retorna um List de objetos ZipEntry.
  • criarZip( File, File[] ) – Cria um arquivo zip com nome e path informado no File fornecido, contendo os arquivos e diretórios no array de objetos File. Adiciona subdiretórios e arquivos, recursivamente.
  • criarZip(java.io.OutputStream, File[]) – Sobrecarga de criarZip(), porém recebendo um stream (em vez de um File) para gravação dos bytes do zip.
  • extrairZip( File, File ) – Extrai o zip informado no primeiro argumento para o diretório informado no segundo. (Lembrando que objetos File podem representar tanto arquivos como diretórios).
  • extrairZip( File ) – Extrai o último zip utilizado pela classe Zipper, no diretório informado como argumento.
  • getArquivoZipAtual() – Retorna a referência para o último arquivo zip usado ou null se não houver um arquivo.
  • fecharZip() – Encerra as referências para o último zip em uso. É usado no programa quando fechamos o arquivo aberto.

A escolha do uso de ZipFile em vez de ZipInputStream na aplicação não foi com feita base em aspectos técnicos ou performáticos, mas foi meramente por um problema detectado durante o desenvolvimento do programa. Se você tentar abrir um arquivo que não seja do formato ZIP com a classe ZipInputStream, nenhuma exceção é lançada e nenhum byte é lido. A classe ZipFile, ao contrário, lança uma exceção caso um arquivo fora do padrão seja aberto, o que, no nosso caso, é importante saber, para que o programa fique consistente e não haja problemas quando o usuário tenta abrir outro tipo de arquivo.

Extração

Para explicar a extração, tomaremos como base o método extrairZip( File, File ). Inicialmente o método verifica se existe o diretório onde o zip será extraído, senão o cria. Caso o diretório especificado seja inválido, é lançada uma exceção:

1 if( !diretorio.isDirectory() )
2   throw new IOException("Informe um diretório válido");
3 else if( !diretorio.exists() )
4   diretorio.mkdirs();

Em seguida é criado um objeto ZipFile, vinculado ao Zip em disco. A partir deste objeto, recuperamos as entradas do Zip com o método entries():

1 zip = new ZipFile( arquivoZip );
2 Enumeration e = zip.entries();

Então extraímos o conteúdo para cada entrada recuperada. Para isso, criamos um objeto File com o diretório onde ele será extraído e o nome do arquivo, baseado no nome da entrada.

Se a entrada representar um diretório inexistente, então criamos o diretório em disco e pulamos para a próxima entrada. Se o File representar um arquivo, verificamos se existe a estrutura de diretórios a qual ele pertence e então a criamos, se não existir. Esse cuidado é necessário porque as entradas extraídas do Zip podem conter, além do nome, um caminho.
Consulte o quadro “java.io.File e Streams” para mais detalhes sobre essas classes.

01 while( e.hasMoreElements() )  {
02   ZipEntry entrada = (ZipEntry) e.nextElement();
03   arquivo = new File( diretorio, entrada.getName() );
04   if( entrada.isDirectory() && !arquivo.exists() ) {
05     arquivo.mkdirs();
06     continue;
07   }
08   if( !arquivo.getParentFile().exists() ) {
09     arquivo.getParentFile().mkdirs();
10   }
11   ...

É dentro do bloco try, logo em seguida ao trecho anterior, que acontece a extração do arquivo. Para cada entrada, por meio do objeto de ZipFile, obtemos um InputStream, informando a entrada a ser recuperada. E criamos um FileOutputStream para gravar o arquivo em disco. Repare que o método getInputStream() recebe como argumento o ZipEntry que vai ser recuperado, então o método retorna o InputStream para e leitura dos dados da entrada especificada, permitindo, desta forma, a leitura aleatória das entradas do Zip.

1 is = zip.getInputStream( entrada );
2 os = new FileOutputStream( arquivo );

No while é feita a leitura dos dados provenientes do InputStream, que então são gravados em disco, através do stream de saída. Ao final da gravação, fechamos os dois streams abertos, dentro do bloco finally. Finalizadas a extração e a gravação em disco de todas as entradas, fechamos o zip com o método close() de ZipFile.

Extraindo com ZipInputStream

A Listagem 4 demonstra como fazer a extração com ZipInputStream. Com ZipInputStream, os dados podem ser lidos diretamente através do método read(), o qual retorna apenas os dados da entrada atual. Veja que esta classe lê o Zip seqüencialmente, entrada a entrada. Para passar à próxima entrada invocamos o método getNextEntry(). Nesse exemplo abrimos um ZipInputStream vinculado a um Zip em disco (usando um BufferedOutputStream).

Listagem 4: Extração de zip com ZipInputStream

01 String arquivo = "c:/teste.zip";
02 final int BUFFER = 2048;
03 FileInputStream fis = new FileInputStream( arquivo );
04 BufferedInputStream bis = new BufferedInputStream( fis, BUFFER );
05 ZipInputStream zis = new ZipInputStream( bis );
06 ZipEntry entrada = null;
07 while( (entrada = zis.getNextEntry()) != null ) {
08   int bytesLidos = 0;
09   byte dados[] = new byte[BUFFER];
10   //grava o arquivo em disco
11   FileOutputStream fos = new FileOutputStream(entrada.getName());
12   BufferedOutputStream dest = new BufferedOutputStream(fos, BUFFER);
13   while( (bytesLidos = zis.read(dados, 0, BUFFER)) != -1 ) {
14   dest.write( dados, 0, bytesLidos );
15   }
16   dest.flush();
17   dest.close();
18   fos.close();
19 }
20 zis.close();
21 bis.close();
22 fis.close();

Criando um Zip

Ainda com base no programa de exemplo, vamos analisar como podemos criar arquivos Zip com Java. Relembrando, a Listagem 3 exibe a classe Zipper que foi criada para encapsular e facilitar o uso das funcionalidades da API de Zip no nosso programa de exemplo, que é baseado no aplicativo Winzip, porém mais simples.

O método criarZip( File, File[] ) da classe Zipper é usada pelo programa de exemplo para cria um Zip em disco, armazenando e compactando os arquivos e diretórios selecionados, incluindo toda a raiz de subdiretórios e arquivos. Este método recebe dois argumentos: o File para o Zip a ser criado e um array de File que contém os arquivos e diretórios a serem compactados e armazenados no Zip.

Acompanhando a Listagem 3, no código do método, podemos ver o que ele faz. Primeiro verifica se o arquivo informado possui a extensão .zip, senão cria um novo File para conter a extensão desejada, baseado no File informado.

1 if( !arquivoZip.getName().toLowerCase().endsWith(".zip") ) {
2   arquivoZip = new File( arquivoZip.getAbsolutePath()+".zip" );
3 }

Depois do passo anterior é criado um stream para o arquivo, que será usado para gravar os bytes em disco.

1 fos = new FileOutputStream( arquivoZip );
2 bos = new BufferedOutputStream( fos, 2048 );

Não é exatamente no método atual que está a lógica de criação do Zip, mas sim na sua sobrecarga, criarZip( OutputStream, File[] ), que recebe o objeto de OutputStream para o Zip e o mesmo array de File recebido como argumento.

Esta sobrecarga foi criada para dar maior usabilidade à classe, pois utilizando esta opção podemos não só gravar o Zip em disco, mas também gravar os dados em qualquer stream de saída, como por exemplo no stream da resposta HTTP, sendo possível enviar ao navegador web um Zip, sem a necessidade de gravá-lo em disco. Mais tarde voltaremos a falar sobre geração de Zip na web.

Voltando à criação do Zip, vamos olhar dentro do método criarZip( OutputStream, File[] ), que é onde o Zip é realmente criado. A primeira instrução no método verifica se o segundo argumento, o array de File, veio como null ou sem itens. Caso verdadeiro ele lança uma exceção informando o erro. Em seguida é criada uma lista, com a classe ArrayList, com o nome da variável listaEntradasZip, que vai armazenar todos os objetos ZipEntry que forem armazenados no Zip, que no final será o retorno do método.

Esta é uma informação a ser retornada apenas para não ter que se abrir e ler o Zip novamente para obter a lista de ZipEntry que o Zip criado contém. No caso da nossa aplicação de exemplo, depois de criado o Zip, será exibida na tela uma lista das entradas armazenadas.

1 if( arquivos == null || arquivos.length < 1 ) {
2   throw new ZipException("Adicione ao menos um arquivo ou diretório");
3 }
4 List listaEntradasZip = new ArrayList();

Continuando no código do método chegamos ao primeiro ponto que interessa de fato na criação do Zip.

É por meio da classe ZipOutputStream que gravamos os dados que serão armazenados e compactados dentro do Zip.

O construtor da classe recebe o OutputStream para onde serão gravados os bytes do Zip. O laço for a seguir percorre cada item do array de File recebido como argumento e, para cada File, pega o caminho onde se encontra o File e passa ao método privado adicionarArquivoNoZip(), junto com o stream e o File a ser adicionado no Zip. É dentro deste método que o File será gravado no stream do Zip. O método foi criado para ser reusado, principalmente recursivamente, para incluir subdiretórios e outros arquivos existentes dentro de um diretório informado.

1 zos = new ZipOutputStream( os );
2 for( int i=0; i

O método adicionarArquivoNoZip() recebe três argumentos: o ZipOutputStream, onde serão gravados os arquivos no Zip, o File a ser gravado e o caminho onde se encontra este File.

Dentro do método, a primeira instrução verifica se o File informado é um diretório. Caso positivo, então é obtida a lista de arquivos e diretórios que existem dentro deste diretório e através do laço for, cada File listado será incluído no Zip, através de uma chamada recursiva ao método adicionarArquivoNoZip(). O retorno deste método será adicionado à lista de entradas adicionadas, que vai compor a lista final completa das entradas do Zip.

01 if( arquivo.isDirectory() ) {
02   //recursivamente adiciona os arquivos dos diretórios abaixo
03   File[] arquivos = arquivo.listFiles();
04   for( int i=0; i

Caso o File não seja um diretório, o fluxo segue normalmente pelo método, e o primeiro passo é calcular o nome completo da entrada no Zip.

Este passo foi considerado, pois não desejamos que a entrada seja adicionada ao zip com o seu caminho completo, por exemplo C:arquivosimagensfoto1.jpg, mas sim contendo apenas o caminho relativo ao local onde o arquivo foi originalmente selecionado, por exemplo imagensfoto1.jpg.

Calculado este caminho, então é criado um ZipEntry que será adicionado ao Zip através do método putNextEntry() de ZipOutputStream.

Quando se adiciona uma entrada a um Zip, deve-se fazer desta forma. Em seguida é chamado o método setMethod(), que define se a entrada deve ser adicionada de forma compactada ou apenas armazenada (sem compactação). Este método aceita um argumento do tipo int, que pode ser representado por duas constantes da classe ZipOutpuStream: DEFLATED (compactada) e STORED (sem compactação).

Seguindo no código, é criado um stream para ler o conteúdo do arquivo a ser adicionado ao Zip, que é lido dentro do laço while e gravado ao ZipOutputStream, através do seu método write(), que segue o padrão dos streams de saída.

Por fim a entrada é adicionada à lista de entradas e depois os streams de leitura do arquivo adicionado são fechados e a lista parcial de entradas adicionadas é retornada.

01 ZipEntry entrada = new ZipEntry( caminhoEntradaZip );
02 zos.putNextEntry( entrada );
03 zos.setMethod( ZipOutputStream.DEFLATED );
04 fis = new FileInputStream( arquivo );
05 bis = new BufferedInputStream( fis, TAMANHO_BUFFER );
06 int bytesLidos = 0;
07 while((bytesLidos = bis.read(buffer, 0, TAMANHO_BUFFER)) != -1) {
08   zos.write( buffer, 0, bytesLidos );
09 }
10 listaEntradasZip.add( entrada );

Neste ponto a execução volta ao método criarZip(), caso não seja uma chamada recursiva.

Ao final, o objeto de ZipOutputStream é fechado, após a inclusão de todas as entradas no Zip. E está finalizada a criação do zip.

Resumindo a criação do Zip: primeiro criamos um ZipOutputStream para o OutputStream onde vamos gravar o Zip (seja em arquivo ou outro meio), depois, para cada entrada a ser adicionada ao Zip, criamos um ZipEntry com o nome (e caminho) da entrada, adicionamos a entrada ao ZipOutputStream com putNextEntry(), então lemos os dados do arquivo a ser adicionado e gravamos no Zip com o método write().

Exemplo:

1 ZipOutputStream zos = new ZipOutputStream( os );
2 ZipEntry entrada = new ZipEntry( "diretório/arquivo.txt" );
3 zos.putNextEntry( entrada );
4 zos.write( bytesDoArquivoTxt );
5 zos.close(); 

Gerando zip na web

Dois problemas recorrentes do protocolo HTTP na internet hoje podem ser solucionados com arquivos Zip.

1) A largura de banda limitada que não permite downloads mais rápidos pode ser, em parte, solucionado com a compactação do conteúdo de arquivos a serem baixados;
2) Outro problema é que o protocolo HTTP não permite download de mais de um arquivo simultaneamente, ou seja, com apenas um request, e isso pode ser solucionado adicionando os vários arquivos em um único Zip e enviando este para o cliente.

Nesta seção vamos demonstrar uma simples aplicação web, com uma página JSP que navega no sistema de arquivos (Imagem 2) da máquina em que se localiza o servidor web e permite fazer o download de um ou mais arquivos de uma só vez, recebido na forma de um Zip, via um Servlet.

Figura 2: Tela do programa web que gera zip

O mais interessante desta aplicação é que o Zip é gerado dinamicamente e enviado ao cliente sem a necessidade de criar um arquivo temporário ou intermediário em disco, ou seja, tudo é feito em memória e enviado ao cliente (geração on-the-fly), evitando acesso ao disco.

O código do JSP que navega no sistema de arquivo está fora do escopo e não será listado no artigo, mas está disponível para download. O que nos interesse de fato é o código do Servlet, que recebe uma requisição com o caminho dos arquivos selecionados para download - através do parâmetro HTTP "arquivo" - gera o Zip e o envia na resposta HTTP.

A Listagem 6 exibe todo o código da classe DownloadServlet, que é um HttpServlet.

Listagem 6: Servlet de download de ZIP

01 package jm.zipper.web;
02 import java.io.*;
03 import javax.servlet.*;
04 import javax.servlet.http.*;
05 
06 import jm.zipper.Zipper;
07 
08 public class DownloadServlet extends HttpServlet {
09   public void doGet(HttpServletRequest req, HttpServletResponse res)
10   throws ServletException, IOException {
11     doPost( req, res );
12   }
13     
14   protected void doPost(HttpServletRequest req, HttpServletResponse res)
15   throws ServletException, IOException {
16   String[] arquivos = req.getParameterValues("arquivo");
17   if( arquivos == null || arquivos.length <= 0 ) {
18     req.setAttribute("msg","Selecione ao menos um arquivo");
19     req.getRequestDispatcher("index.jsp").forward( req, res );
20     return;
21   }
22   res.setContentType("application/octet-stream");
23   res.setHeader("Content-Disposition", "attachment; filename=arquivos.zip");
24   gerarZip( res.getOutputStream(), arquivos );
25   }
26   
27   private void gerarZip( OutputStream os, String[] nomesArquivos ) throws IOException {
28   File[] arquivos = new File[ nomesArquivos.length ];
29   for( int i=0; i

O Servlet atende apenas a requisições GET e POST, pois implementa apenas os métodos doGet() e doPost(). Dentro do método obtemos um array de strings, que será o caminho absoluto dos arquivos e diretórios selecionados no JSP. A partir deste array verificamos se foi selecionado pelo menos um item, senão, uma mensagem de erro é enviada e exibida de volta no JSP.

1 String[] arquivos = req.getParameterValues("arquivo");
2 if( arquivos == null || arquivos.length <= 0 ) {
3   req.setAttribute("msg","Selecione ao menos um arquivo");
4   req.getRequestDispatcher("index.jsp").forward( req, res );
5   return;
6 }

Caso haja sido feita a seleção, o programa faz duas coisas importantes no ambiente web.

Primeiro define o conteúdo da resposta, ou seja, o content-type, que no caso é definido como application/octet-stream. Depois definimos um cabeçalho (Content-Disposition) que diz ao navegador que estamos enviando um arquivo anexado (attachment) e que o navegador deve abrir a janela perguntando se o usuário deseja abrir ou salvar o arquivo enviado.

Depois obtemos o stream para a resposta HTTP e então chamamos o método gerarZip() do próprio Servlet, que é quem vai gerar o Zip, através da classe Zipper, que criamos para o programa de exemplo anterior.

1 res.setContentType("application/octet-stream");
2 res.setHeader("Content-Disposition", "attachment; filename=arquivos.zip");
3 OutputStream os = res.getOutputStream();
4 gerarZip( os, arquivos );

Repare que este método gerarZip() recebe dois argumentos: o stream para a resposta HTTP e o array com os arquivos selecionados. Neste método criamos um array de File, a partir do array de string, que será um dos argumentos do método criarZip() da classe Zipper.

Recorde que vimos o método criarZip() na seção anterior do artigo e vimos que este método gera um Zip diretamente em um OutputStream ao invés de em um arquivo em disco. O que ocorre é que gravamos os bytes do Zip diretamente na resposta HTTP que será recebido pelo navegador, que reconhecerá o arquivo e dará a opção de abrir ou salvar o arquivo recebido. Por fim é feito um flush() no stream para efetivar o envio dos bytes ao cliente.

O arquivo disponível para download no site contém um arquivo WAR (jm-zipper.war) dentro do diretório deploy, que pode ser instalado em qualquer container web ou J2EE disponível.

Coloque este WAR no local correto para o seu container (no caso do Tomcat, copie o WAR para o diretório webapps) e o inicie. Abra o seu navegador preferido e aponte para https://localhost:8080/jm-zipper/.

Caso o seu servidor esteja configurado em outra máquina ou porta, altere os valores necessários.

Trabalhando com GZIP

Alternativamente ao formato ZIP, podemos criar e manipular arquivos GZIP, porém este último formato não permite armazenamento de múltiplos arquivos, mas apenas de um arquivo por vez.

Para mais detalhes sobre as diferenças entre os formatos Zip e Gzip, consulte o quadro "ZIP versus GZIP".

Para criar e extrair conteúdo do GZIP usamos as classes GZIPInputStream e GZIPOutputStream, respectivamente.

A Listagem 7 mostra um programa que cria um arquivo GZIP e depois extrai o conteúdo do mesmo GZIP criado. Perceba que a criação e extração é meramente uma questão de abrir um stream e gravar ou obter os dados.

Listagem 7: Manipulando arquivos GZIP

01 package jm.zipper;
02 import java.io.*;
03 import java.util.zip.*;
04 
05 public class ExemploGZip {
06   public static void main(String[] args) throws Exception {
07   int TAMANHO_BUFFER = 2048; //2 KBytes
08   byte[] dados = new byte[TAMANHO_BUFFER];
09 
10   File arquivo = new File("c:/teste.txt");
11   File arquivo2 = new File("c:/teste2.txt");
12   File arquivoGzip = new File("c:/teste.gz");
13 
14   //cria o GZIP
15   OutputStream os = new FileOutputStream( arquivoGzip );
16   GZIPOutputStream gos = new GZIPOutputStream( os );
17   InputStream is = new FileInputStream( arquivo );
18   int bytesLidos = 0;
19   while( (bytesLidos = is.read( dados, 0, TAMANHO_BUFFER )) > 0 ) {
20     gos.write( dados, 0, bytesLidos );
21   }
22   is.close();
23   gos.close();
24   os.close();
25   
26   //extrai o GZIP
27   InputStream is2 = new FileInputStream( arquivoGzip );
28   GZIPInputStream gis = new GZIPInputStream( is2 );
29   OutputStream os2 = new FileOutputStream( arquivo2 );
30   bytesLidos = 0;
31   while( (bytesLidos = gis.read( dados, 0, TAMANHO_BUFFER )) > 0 ) {
32     os2.write( dados, 0, bytesLidos );
33   }
34   os2.close();
35   gis.close();
36   is2.close();
37   }
38 } 

ZIP versus GZIP

Os usuários do Windows estão familiarizados com o formato ZIP, que serve tanto para armazenar arquivos, como para comprimir dados.

Os usuários Linux/Unix usam também o formato GZIP, que apenas comprime dados, não servido como arquivador, pois este formato não permite mais de um arquivo armazenado nele. Geralmente usuários Linux usam duas ferramentas: o tar, para gerar um arquivo que arquiva (armazena) diversos outros arquivos e o gzip para compactar o arquivo tar gerado, criando arquivos com a extensão no padrão tar.gz.

java.io.File e Streams

A classe java.io.File é uma representação abstrata para o caminho de um arquivo ou diretório, independente do sistema operacional ou sistema de arquivos utilizado.

Esta classe possui muitos métodos úteis para a obtenção de informações do arquivo em disco, verificar se ele existe, se é arquivo ou diretório, criar a estrutura de diretórios representada, criar um arquivo vazio em disco, listar os diretórios e arquivos etc.

Para mais detalhes da classe, consulte a documentação em: https://java.sun.com/j2se/1.4.2/docs/api/java/io/File.html.

Os streams (ou fluxos) são a base da comunicação dos dados. Eles são como os dutos que transportam os bytes de um lado para o outro.

Stream é um conceito genérico para o transporte de dados, que é utilizado em diversas necessidades como acesso a arquivo em disco, comunicação de rede via sockets etc.

Basicamente existem dois tipos de streams: os de entrada, chamados de input stream; e os de saída, chamados output stream.

Os streams de entrada servem para ler ou receber dados oriundos de alguma fonte, já os de saída, consequentemente, servem para enviar ou gravar dados para outro destino.

Em comum, as classes que implementam os streams de entrada fornecem o método read() para leitura dos dados e as classes que implementam os stream de saída fornecem o método write() para gravar os dados.

Para estar dentro do padrão de streams do Java, toda classe que é um stream de entrada deve implementar a interface java.io.InputStream, direta ou indiretamente.

O mesmo vale para as classes de stream de saída, que devem implementar a interface java.io.OutputStream.

As classes BufferedInputStream e BufferedOutputStream implementam o conceito de buffer para a leitura e gravação de bytes via streams.

O uso dessas classes torna os acesso mais performáticos, pois trabalham com os bytes de dados em memória e evita o acesso direto a todo instante ao disco, por exemplo.

Mais informações, consulte o tutorial oficial da Sun: https://java.sun.com/docs/books/tutorial/essential/TOC.html#io

Conclusões

O artigo demonstrou através de exemplos a utilização do pacote java.util.zip para a leitura, extração e criação de arquivos ZIP, além de uma aplicação de exemplo no estilo Winzip e como podemos gerar arquivos zip on-the-fly na web.

Para finalizar, foi demonstrado também como se trabalhar com arquivos GZIP.
Agora você tem o conhecimento necessário para trabalhar com compactação Zip de arquivos, que poderá ajudar muito a enriquecer suas aplicações.

É interessante que o leitor baixe o conteúdo disponível para download, pois assim você pode verificar todos os fontes, além de abrir o projeto na IDE Eclipse (versão utilizada 3.1) ou uma outra de sua preferência - será necessário criar um novo projeto para outra IDE. Dentro do diretório deploy é disponibilizado o JAR com os binários do artigo e o programa de exemplo. Para iniciar o programa, dê um duplo-clique com o mouse no JAR ou execute a seguinte linha de comando:

java -jar jm-zipper.jar