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 ZipEntryMÉ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.java01 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<ZipEntry> 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.java01 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<arquivos.length; i++ ) {
132 String caminhoInicial = arquivos[i].getParent();
133 List novasEntradas = adicionarArquivoNoZip( zos, arquivos[i], caminhoInicial );
134 if( novasEntradas != null ) {
135 listaEntradasZip.addAll( novasEntradas );
136 }
137 }
138 }
139 finally {
140 if( zos != null ) {
141 try {
142 zos.close();
143 } catch( Exception e ) {}
144 }
145 }
146 return listaEntradasZip;
147 }
148
149 private List adicionarArquivoNoZip( ZipOutputStream zos, File arquivo, String caminhoInicial ) throws IOException {
150 List listaEntradasZip = new ArrayList();
151 FileInputStream fis = null;
152 BufferedInputStream bis = null;
153 byte buffer[] = new byte[TAMANHO_BUFFER];
154 try {
155 //diretórios não são adicionados
156 if( arquivo.isDirectory() ) {
157 //recursivamente adiciona os arquivos dos diretórios abaixo
158 File[] arquivos = arquivo.listFiles();
159 for( int i=0; i<arquivos.length; i++ ) {
160 List novasEntradas = adicionarArquivoNoZip( zos, arquivos[i], caminhoInicial );
161 if( novasEntradas != null ) {
162 listaEntradasZip.addAll( novasEntradas );
163 }
164 }
165 return listaEntradasZip;
166 }
167 String caminhoEntradaZip = null;
168 int idx = arquivo.getAbsolutePath().indexOf(caminhoInicial);
169 if( idx >= 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 ZipperMÉ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 ZipInputStream01 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<arquivos.length; i++ ) {
3 String caminhoInicial = arquivos[i].getParent();
4 List novasEntradas = adicionarArquivoNoZip( zos, arquivos[i], caminhoInicial );
5 if( novasEntradas != null ) {
6 listaEntradasZip.addAll( novasEntradas );
7 }
8 }
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<arquivos.length; i++ ) {
05 List novasEntradas = adicionarArquivoNoZip( zos, arquivos[i], caminhoInicial );
06 if( novasEntradas != null ) {
07 listaEntradasZip.addAll( novasEntradas );
08 }
09 }
10 return listaEntradasZip;
11 }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:\arquivos\imagens\foto1.jpg, mas sim contendo apenas o caminho relativo ao local onde o arquivo foi originalmente selecionado, por exemplo
imagens\foto1.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 ZIP01 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<arquivos.length; i++ ) {
30 arquivos[i] = new File( nomesArquivos[i] );
31 }
32 Zipper zipper = new Zipper();
33 zipper.criarZip( os, arquivos );
34 os.flush();
35 }
36 }
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
http://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 GZIP01 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:
http://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:
http://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