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