sobota 5. února 2011

Zipujeme efektivně

Jedna ze základních vlastností Javy je práce se ZIP archívy, ať už jsou to knihovny tříd a spustitelné JARy, webové aplikace (war), nebo třeba JEE bumbrlíčci (ear). Není tedy divu, že i přímo v základním API je implementována práce s těmito archívy. Slouží k tomu třídy v balíku java.util.zip a nejzajímavější z nich jsou ZipOutputStream a ZipInputStream.

Příkladem budiž vytvoření zipu:

//Vytvorime Zip
ZipOutputStream zos = new ZipOutputStream(new FileOutputStream("javlog.zip"));
//V Zipu chceme mit jeden textovy soubor
zos.putNextEntry(new ZipEntry("priznani.txt"));
//naplnime obsah textoveho souboru
zos.write("Máme rádi Javu!".getBytes());
//zavrem entry (priznani.txt)
zos.closeEntry();
//zavrem stream
zos.close();
a jeho rozbalení:
ZipInputStream zis = new ZipInputStream(new FileInputStream("javlog.zip"));
ZipEntry zipEntry;
//budem predpokladat, ze v ZIPu mame jen textove soubory a tak je vypisem do konzole
while ((zipEntry = zis.getNextEntry()) != null) {
    System.out.println("Soubor: " + zipEntry.getName());
    BufferedReader br = new BufferedReader(new InputStreamReader(zis));
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
    System.out.println();
    zis.closeEntry();
}
zis.close();

Jak je vidět, není tenhle přístup úplně přímočarý. Tím pádem je i větší prostor pro zavlečení nějaké chybky. Víte například, jak rozlišíte při vytváření ZIPu prázdný soubor od prázdného adresáře?

Tento základní přístup se často hodí v případě, kdy obsah, který chcete do zipu vložit, nemáte ve file systému, ale např. ho generujete přímo ve webové aplikaci. Pak daný ZipOutputStream vytvoříte nad OutputStreamem servletu a naládujete do něj data přímo z paměti nebo z databáze.

To co vývojář aplikace často potřebuje, je možnost jednoduše zazipovat část souborového systému (složku a všechno co je v ní), a nebo ze ZIPu zase všechno někam rozbalit. Samozřejmě, že se dají na internetu najít knihovny, které tuto funkcionalitu poskytují, ale částo jsou kanón na vrabce (opravdu potřebujete aby knihovna uměla i bzip2, 7z, arj a cab?), nebo naopak příliš jednoduché a nezvládnou operaci typu – sbal mi část filesystému a k tomu přidej tenhle vygenerovaný (v paměti) PDF report.

Takže by se v těchto případech hodilo malé rozšíření Zip*Stream tříd. Aby na jednu stranu bylo možné jednoduše pracovat přímo se souborovým systémem a na druhou, aby nebyl programátor ochuzen o to, co mu ZipStreamy umožňují. Hodily by se nám tedy metody:

void ZipOutputStream.put(File fileOrFolderToZip};
void ZipInputStream.unzip(File folderWhereToStoreUnzippedFiles);

A protože originální Zip*Stream třídy nejsou finální, nic nám nebrání v rozšíření. Má implementace přidává ještě dodatečné parametry, aby bylo použití univerzálnější.

cz.cacek.javlog.zip.ZipOutputStream extends java.util.zip.ZipOutputStream:

/**
 * Adds file or directory (with its content) to this {@link ZipOutputStream}.
 * If the given {@link File} doesn't exist, it does nothing and returns.
 * The path of the given {@link File} in the result zip is determined only
 * from the file name
 * and from the provided basePath. It doesn't take the file path into consideration 
 * (but the folder structure under the given folder is preserved).
 * 
 * @param file
 *            {@link File} (normal or directory) to add to the stream
 *            (must not be null)
 * @param basePath
 *            base path (path prefix) for added {@link File} (may be null)
 * @param contentOnly
 *            determines if the folder name (in case the file
 *            parameter is directory) should be added to the zip path. If
 *            true is provided only the folder content is added and the
 *            folder itself is not included.
 * @throws IOException
 */
public void put(final File file, final String basePath, boolean contentOnly) throws IOException;
cz.cacek.javlog.zip.ZipInputStream extends java.util.zip.ZipInputStream:
/**
 * Unzips the stream to the given base folder. If the baseFolder parameter
 * is null, the default (working) directory is used instead. If the
 * baseFolder doesn't exist it is created.
 * 
 * @param baseFolder
 *            base folder to unpack (may be null).
 * @param allowAbsolutePath
 *            flag which says if handling of absolute paths is allowed in
 *            the zip. If the flag is
 *            true and ZipEntry contains absolute path, the baseFolder
 *            parameter is not used and File is created from the absolute
 *            path in the ZipEntry.
 * @throws IllegalArgumentException
 *             base folder can't be created or file denoted by baseFolder
 *             parameter already exists,
 *             but it's not a directory.
 * @throws IOException
 */
public void unzip(final File baseFolder, final boolean allowAbsolutePath) throws IllegalArgumentException, IOException;

A použití? Chceme například rozbalit war soubor do nějaké složky, upravit obsah a zase ho zabalit:

final File baseFolder = new File("temp-webapp"); //folder to unpack the zip content (will be created)
final File warFile = new File("myapp.war"); //file to unpack

//unpack ZIP
ZipInputStream zis = new ZipInputStream(new FileInputStream(warFile));
zis.unzip(baseFolder, false);
zis.close();

//TODO make some magic with files in the temp-webapp folder
new FileOutputStream(new File(baseFolder, "newEmptyFile.xxx")).close();

//repack the ZIP (WAR) file
ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(warFile));
zos.put(baseFolder, null, true); //pack only the content of the folder - 3rd parameter is true
zos.close();

//TODO remove the baseFolder
//delete(baseFolder);

Toť vše, ani to nebolelo.

Upravené ZipStreamy připravené k použití najdete v tomto jaru (3 třídy + zdrojáky), nebo v zazipovaném Eclipse projektu i s JUnit testy.

A co používáte k práci s ZIP archívy vy? Podělte se v komentářích.

5 komentářů:

NkD řekl(a)...

Sranda s timhle zacina, kdyz maji nazvy souboru nejakou interpunkci a nejlepe treba kus polske a kus ceske. To se pak deji kouzla po rozbaleni. Viz treba: http://www.etnetera.cz/cz/773-tech_life/tech_life_zip_file.html

Arnost řekl(a)...

K praci se ZIP jsem nedavno pouzil TrueZIP (http://truezip.java.net/).

Ma celkem pouzitelne API, ale vlastnost ktera se mi libila nejvic je schopnost pracovat se ZIPem jako s adresarem.

Funguje to i rekurzivne - ZIP zazipovany v ZIPu ktery je zazipovany v ZIPu. Idealni pro problem ktery jsem resil - otevrit EAR, najit WAR a precist a zapsat JAR v /WEB-INF/lib/ (vsechny 3 jsou vlastne ZIP)

Josef Cacek řekl(a)...

@NkD: různé charsety použité pro jména souborů v zipu jsou opravdu na odstřel - kdyby se alespoň někde v zipu zapisovala informace o použitém kódování, hned by se žilo radostněji;
Když se pracuje jen v Javě na straně vytváření archivu i rozbalování, tak to je v pohodě - prostě tam bude UTF-8. Ale když to pak chce člověk rozbalit na českých Windows ... tak se ucho utrhlo.

@Arnost: Možnosti Truezipu vypadadají dobře. Pro mě je to v současné době onen zmíněný kanón na vrabce, ale je dobré o tomhle projektu vědět.

Michal řekl(a)...

Někdy je výhodné i použití Commons VFS http://commons.apache.org/vfs/

Anonymní řekl(a)...

Cuzz chlapci, taky pridam svuj dil. Nedavno jsme si psali anti task do mavenu, ktery nahrazoval v EARu deployment descriptory, to je asi fuk, ale pekne jsme si nabehli s lomitkama v absolut path pri zpetnem zipovani. Vsechno se tvari ok, az do chvile, kdy se to zkompiluje na windows a deployuje na unixu.... bacha na to.