21.8.13

ZIP sin compresión

Crear un fichero ZIP en Java es bastante fácil. No tenemos más que usar las clases ZipOutputStream y ZipEntry del paquete java.util.zip. Cada fichero dentro del ZIP está delimitado por una ZipEntry, que debemos crear y añadir al objeto ZipOutputStream, mediante su método putNextEntry(ZipEntry). Una vez añadida la ZipEntry, se escribe en el ZipOutputStream los datos deseados, y finalmente se cierra la entrada con una llamada al método closeEntry() del ZipOutputStream. Repetimos el proceso tantas veces como entradas tengamos que añadir, y terminamos con la inevitable llamada al close() del ZipOutputStream.

Veamos un ejemplo muy sencillo:


import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

public class SimpleExample {

    public static void main(String[] args) throws IOException {
        File file = new File("example.zip");
        ZipOutputStream zipOutputStream = new ZipOutputStream(new FileOutputStream(file));

        addEntry(zipOutputStream, "example1.txt", "Some text".getBytes());
        addEntry(zipOutputStream, "example2.txt", "More text".getBytes());

        zipOutputStream.close();
    }

    private static void addEntry(ZipOutputStream zipOutputStream, String entryName, byte[] entryData) throws IOException {
        ZipEntry zipEntry = new ZipEntry(entryName);
        zipOutputStream.putNextEntry(zipEntry);
        zipOutputStream.write(entryData);
        zipOutputStream.closeEntry();
    }
}

Ambas clases tienen un método setMethod(int), que indica el método de compresión para los datos. Podemos pasar como argumento ZipEntry.DEFLATED (usar compresión) o ZipEntry.STORED (no usar compresión). Si usamos el setMethod(int) de ZipOutputStream, afectará a todas las entradas subsiguientes, mientras que si utilizamos el setMethod(int) de ZipEntry, afectará únicamente a esa entrada.

Si por el motivo que sea necesitamos entradas que no estén comprimidas (bien algunas en concreto, o todas las del ZIP), debemos llamar a setMethod(int) pasando como argumento ZipEntry.STORED.

¿Y ya está? Pues no, porque de ser así, no estaría escribiendo este post. Si en nuestro ejemplo anterior, simplemente modificamos el método addEntry de esta forma:


    private static void addEntry(ZipOutputStream zipOutputStream, String entryName, byte[] entryData) throws IOException {
        ZipEntry zipEntry = new ZipEntry(entryName);
        zipEntry.setMethod(ZipEntry.STORED);
        zipOutputStream.putNextEntry(zipEntry);
        zipOutputStream.write(entryData);
        zipOutputStream.closeEntry();
    }

nos encontraremos con la siguiente y desagradable sorpresa:


Exception in thread "main" java.util.zip.ZipException: STORED entry missing size, compressed size, or crc-32
 at java.util.zip.ZipOutputStream.putNextEntry(ZipOutputStream.java:167)
 at SimpleExample.addEntry(SimpleExample.java:24)
 at SimpleExample.main(SimpleExample.java:15)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
 at java.lang.reflect.Method.invoke(Method.java:597)
 at com.intellij.rt.execution.application.AppMain.main(AppMain.java:120)

Process finished with exit code 1
  

¿Por qué ocurre esto? Bueno, la clase ZipEntry tiene una serie de atributos que se ajustan automáticamente cuando se usa el método DEFLATED, pero no al usar STORED. Por alguna decisión de diseño que no comprendo, y que no se avisa en ningún sitio del API, al usar STORED hay que ajustar manualmente dichos atributos, llamando a los métodos setSize(long), setCompressedSize(long) y setCrc(long).

¿Qué valores debemos usar? Para los dos primeros es inmediato: el número de bytes de los datos que queremos escribir (ya que al no usar compresión, size y compressedSize, coinciden). Si usamos un array de bytes, basta con leer su propiedad length. Para el último, hay que calcular el CRC-32 de los datos, usando la clase CRC32 (que está en el mismo paquete java.util.zip). Así, la versión modificada de nuestro método de ejemplo addEntry, quedaría así:


    private static void addEntry(ZipOutputStream zipOutputStream, String entryName, byte[] entryData) throws IOException {
        ZipEntry zipEntry = new ZipEntry(entryName);

        zipEntry.setMethod(ZipEntry.STORED);
        zipEntry.setSize(entryData.length);
        zipEntry.setCompressedSize(entryData.length);

        CRC32 crc = new CRC32();
        crc.update(entryData);
        zipEntry.setCrc(crc.getValue());

        zipOutputStream.putNextEntry(zipEntry);
        zipOutputStream.write(entryData);
        zipOutputStream.closeEntry();
    }

No es demasiado complicado, pero hemos tenido que añadir 6 líneas (en vez de sólo una), y calcular nosotros el CRC de los datos. Además, si en vez de tener los datos a escribir en un array de bytes, lo tenemos que leer de un InputStream, la cosa se complica un poco, ya que para conocer el número de bytes y calcular el CRC, debemos recorrer el stream, y posteriormente deberemos recorrerlo nuevamente para escribirlo en el ZipOutputStream. Y si el InputStream es de un solo uso, tendríamos que ir copiando los bytes en algún otro lugar durante la primera pasada, para pasárselos luego al ZipOutputStream.

No hay comentarios:

Publicar un comentario