Compresión y Decompresión de archivos ZIP con Java

En este artículo el autor nos presenta las clases ZipInputStream, ZipOutputStream y ZipEntry del paquete java.util.zip estándar en Java para la manipulación de archivos ZIP (o GZIP). Ofrece ejemplos de código para la creación de archivos ZIP, su descompresión, y la visualización de su contenido.

¿Has tratado alguna vez de abrir un archivo JAR utilizando WinZip o algún otro visualizador gráfico de archivos ZIP? Cuando archivos Java son agrupados y comprimidos en un archivo JAR, la forma de compresión utilizada es ZIP.

Java nos provee la herramienta JAR (en el directorio bin/ del JRE) para crear, ver, y extraer archivos JAR. Pero, aún más útil, también podemos encontrar todo un paquete conteniendo clases que nos permiten agregar compresión ZIP y GZIP a nuestras aplicaciones.

A través del paquete java.util.zip, Sun nos ofrece las siguientes funcionalidades:

  • Compresión, Decompresión, y Visualización de archivos ZIP y GZIP
  • Compresión y Decompresión usando el algoritmo de compresión DEFLATE (usado por ZIP y GZIP)
  • Clases de utilidad para calcular checksums CRC-32 y Adler-32

En este artículo, nos focalizaremos en la compresión, decompresión y visualización de archivos ZIP.

Compresión ZIP

Para permitirnos comprimir múltiples archivos en un solo archivo ZIP, Java nos provee de dos clases:

  • java.util.zip.ZipOutputStream: un wrapper output stream que sabe cómo escribir en un stream destino (usualmente un archivo) aplicando compresión ZIP.
  • java.util.zip.ZipEntry: usado para identificar la ubicación (path) en un archivo ZIP hacia donde el archivo comprimido será escrito.

Los pasos requeridos para comprimir archivos en un archivo ZIP son los siguientes:

  1. Crear un ZipOutputStream que referencia el destino (por ejemplo, puede referenciar a un archivo al wrapear un java.io.FileOutputStream)
  2. Crear un java.io.InputStream (o derivado) que referencia un archivo a agregar al ZIP
  3. Crear una ZipEntry que representa el InputStream creado en el paso 2
  4. Agregar el ZipEntry al ZipOutputStream llamando a su método putNextEntry()
  5. Copiar datos del InputStream al ZipOutputStream (usando un buffer array de bytes para leer desde el InputStream y luego escribirlo en el ZipOutputStream)

 

La clave para crear una compresión de múltiples archivos radica en ZipEntry. El ZipEntry nos permite setear un path lógico dentro del archivo donde el siguiente conjunto de datos binarios será escrito. Luego, cuando el archivo es descomprimido, tiene que recrear la ubicación destino y extraer la entrada allí mismo.

La siguiente porción de código ejemplifica como comprimir una java.util.List de nombres de archivos a un archivo ZIP destino filename.

      try
{
// Reference to the file we will be adding to the zipfile
BufferedInputStream origin = null;
// Reference to our zip file
FileOutputStream dest = new FileOutputStream( filename );
// Wrap our destination zipfile with a ZipOutputStream
ZipOutputStream out = new ZipOutputStream( 
new BufferedOutputStream( dest ) ); // Create a byte[] buffer that we will read data
// from the source // files into and then transfer it to the zip file byte[] data = new byte[ BUFFER_SIZE ]; // Iterate over all of the files in our list for( Iterator i=files.iterator(); i.hasNext(); ) { // Get a BufferedInputStream that we can use to read the
// source file String filename = ( String )i.next(); System.out.println( "Adding: " + filename ); FileInputStream fi = new FileInputStream( filename ); origin = new BufferedInputStream( fi, BUFFER_SIZE ); // Setup the entry in the zip file ZipEntry entry = new ZipEntry( filename ); out.putNextEntry( entry ); // Read data from the source file and write it out to the zip file int count; while( ( count = origin.read(data, 0, BUFFER_SIZE ) ) != -1 ) { out.write(data, 0, count); } // Close the source file origin.close(); } // Close the zip file out.close(); } catch( Exception e ) { e.printStackTrace(); }

En este código, tenemos una java.util.List contieniendo Strings con el path completo a los archivos a agregar en el ZIP. Una ZipEntry es agregada al ZipOutputStream para cada archivo. El archivo es leído al crear un nuevo FileInputStream, referenciándolo y wrapeándolo con un BufferedInputStream. Los datos son luego leídos desde el archivo fuente en bloques de tamaño BUFFER_SIZE y escritos al ZipOutputStream. Luego de que el archivo ha sido copiado completamente al ZipOutputStream, se cierra, y el siguiente archivo es procesado. Esto contininúa hasta que no hay más archivos por procesar. Finalmente, el ZipOutputStream se cierra y la operación es finalizada.

Decompresión ZIP

La decompresión es justamente lo opuesto a la compresión (obviamente, pero me estoy refiriendo a las operaciones de programación!). Leemos un archivo comprimido con ZIP utilizando java.util.ZipInputStream y navegamos a través de su conjunto de ZipEntrys. Los pasos son los siguientes:

  1. Crear un java.util.zip.ZipInputStream que referencia a la fuente comprimida (por ejemplo, para leer un archivo debemos hacer que el ZipInputStream wrapee un java.io.FileInputStream)
  2. Iterar sobre todas las ZipEntrys contenidas en el archivo al llamar al método getNextEntry() de ZipInputStream
  3. Crear un java.io.OutputStream que referencia el destino (por ejemplo, un FileOutputStream) que coincide con el path del ZipEntry
  4. Copiar los datos desde el ZipInputStream hacia el OutputStream, hasta que el stream finalize (esto señala el fin del ZipEntry)
  5. Cerrar el OutputStream
  6. Obtener el siguiente ZipEntry (volviendo al paso 2)

La siguiente porción de código muestra cómo descomprimir archivos de un archivo ZIP hacia sus ubicaciones apropiadas en el sistema de archivo. Asume que el nombre del archivo ZIP ha sido especificado a través de filename, y que el directorio destino ha sido especificado con destination.

       try {
// Create a ZipInputStream to read the zip file
BufferedOutputStream dest = null;
FileInputStream fis = new FileInputStream( filename );
ZipInputStream zis = new ZipInputStream( 
new BufferedInputStream( fis ) ); // Loop over all of the entries in the zip file int count; byte data[] = new byte[ BUFFER_SIZE ]; ZipEntry entry; while( ( entry = zis.getNextEntry() ) != null ) { if( !entry.isDirectory() ) { String entryName = entry.getName(); prepareFileDirectories( destination, entryName ); String destFN = destination + File.separator + entry.getName(); // Write the file to the file system FileOutputStream fos = new FileOutputStream( destFN ); dest = new BufferedOutputStream( fos, BUFFER_SIZE ); while( (count = zis.read( data, 0, BUFFER_SIZE ) ) != -1 ) { dest.write( data, 0, count ); } dest.flush(); dest.close(); } } zis.close(); } catch( Exception e ) { e.printStackTrace(); }

Como en el ejemplo anterior, BUFFER_SIZE puede ser 8192 (8 K), pero esta configuración puede ser cambiada a gusto. El método getName() de ZipEntry retorna el nombre completo, incluyendo el path, hacia el archivo destino.

Un método de ayuda asegura que el directorio existe (o lo crea si no existe) antes de escribir el archivo de salida:

prepareFileDirectories()

Visualización de ZIP

Comparado a la decompresión de archivos ZIP, la visualización de los contenidos es casi tan trivial. Todo lo que necesitamos hacer es crear un ZipInputStream, apuntarlo a nuestro archivo ZIP, e iterar sobre los ZipEntrys sin necesidad de extraer nada. La siguiente porción de código lo ejemplifica.

    try
{
FileInputStream fis = 
new FileInputStream( this.filename ); ZipInputStream zis = new ZipInputStream(
new BufferedInputStream( fis ) ); // Loop over all of the entries in the zip file ZipEntry entry; DateFormat df = DateFormat.getDateTimeInstance(
DateFormat.SHORT, DateFormat.SHORT ); while( ( entry = zis.getNextEntry() ) != null ) { if( !entry.isDirectory() ) { System.out.println( entry.getName() + ", " + df.format( new Date( entry.getTime() ) ) + ", " + entry.getSize() + ", " + entry.getCompressedSize() ); } } } catch( Exception e ) { e.printStackTrace(); }

Este código asume que el archivo ZIP está representado por filename. Crea un FileInputStream que referencia al archivo, lo wrapea con un ZipInputStream, y luego itera sobre los contenidos usando el método getNextEntry() de ZipInputStream. Luego muestra información del archivo utilizando un sobconjunto de la información que provee la clase ZipEntry.

He aquí lo que podemos encontrar en la clase ZipEntry:

  • String getComment(): Retorna el comentario para la entrada, o null si no posee.
  • long getCompressedSize(): Retorna el tamaño de los datos comprimidos para la entrada, o -1 si es desconocido.
  • long getCrc(): Retorna el checksum CRC-32 de los datos de la entrada descomprimida, o -1 si es desconocido.
  • byte[] getExtra(): Retorna la información extra para esta entrada, o null si no posee.
  • int getMethod(): Retorna el método de compresión para esta entrada, o -1 si no está especificado.
  • String getName(): Retorna el nombre de la entrada.
  • long getSize(): Retorna el tamaño descomprimido de los datos de entrada, o -1 si es desconocido.
  • long getTime(): Retorna la fecha y hora de modificación de la entrada, o -1 si no está especificado.
  • boolean isDirectory(): Retorna true si esta es una entrada de directorio.
       

    Conclusión

    La compresión y decompresión de archivos ZIP ha sido abstraída del desarrollador a través del uso de las clases ZipOutputStream para comprimir y escribir datos a un destino, y ZipInputStream para leer y descomprimir los datos.

    Además de la habilidad de comprimir archivos, el otro aspecto de los archivos ZIP es que pueden contener múltiples archivos, por lo que requieren una estructura de catálogo. Esta estructura de catálogo es implementada a través de las ZipEntrys contenidas dentro de las fuentes y los destinos ZIP, y un mecanismo de iteración para navegar esas entradas.

    Steven Haines
    http://cricava.com/java/detalleArticulos.php?id=257

  • One Comment

    1. Xito

      Hola estimado.

      Excelente explicación, pero me podrías facilitar el código completo para poder utilizarlo por favor?.

      Saludos y gracias.

    Leave A Comment?