10.6.2 | Komprimierung im Zip-Format |
Der Umgang mit Daten im Zip-Format läuft etwas anders ab, als bei GZIP. Das liegt schon allein daran, dass man im Zip-Format nicht nur eine Datei komprimiert speichern kann, sondern die Möglichkeit hat, mehrere Dateien zu einem Archiv zusammenzufassen. Die Komprimierung der Daten im Archiv ist optional.
Für das Lesen bzw. Schreiben von Daten im Zip-Format werden die Klassen ZipInputStream bzw. ZipOutputStream benötigt. Zusätzlich wird die Klasse ZipEntry verwendet. ZipEntry repräsentiert einen Eintrag im Zip-Archiv.
Um die Verwendung der oben genannten Klassen zu veranschaulichen, wird in diesem Abschnitt eine Java-Implementierung der zip und unzip Kommandos vorgestellt. Analog zur Implementierung von gzip und gunzip im letzten Abschnitt wird auch hier auf eine Realisierung der Kommandozeilen-Parameter verzichtet, da das Beispiel sonst unnötig komplex werden würde. Rekursives Archivieren von Unterverzeichnissen wird ebenfalls nicht implementiert.
Folgendes Listing stellt die komplette Reimplementierung des zip-Kommandos dar:import java.util.zip.*; import java.io.*; public class Zip { public static void main(String args[]) { PrintWriter stdout = new PrintWriter(System.out, true); int read = 0; FileInputStream in; byte[] data = new byte[1024]; try { // Zip-Archiv mit Stream verbinden ZipOutputStream out = new ZipOutputStream(new FileOutputStream(args[0])); // Archivierungs-Modus setzen out.setMethod(ZipOutputStream.DEFLATED); // Hinzufügen der einzelnen Einträge for (int i=1; i < args.length; i++) { try { stdout.println(args[i]); // Eintrag für neue Datei anlegen ZipEntry entry = new ZipEntry(args[i]); in = new FileInputStream(args[i]); // Neuer Eintrag dem Archiv hinzufügen out.putNextEntry(entry); // Hinzufügen der Daten zum neuen Eintrag while((read = in.read(data, 0, 1024)) != -1) out.write(data, 0, read); out.closeEntry(); // Neuen Eintrag abschließen in.close(); } catch(Exception e) { e.printStackTrace(); } } out.close(); } catch(IOException ex) { ex.printStackTrace(); } } }Das Programm erwartet als ersten Kommandozeilen-Parameter den Namen des Archivs, in dem die Daten gespeichert werden sollen. Dahinter können beliebig viele Dateinamen übergeben werden, deren Dateien in das Archiv aufgenommen werden sollen. Ein Aufruf könnten z. B. folgendermaßen lauten:java Zip demos.zip *.javaAlle java-Dateien werden nach diesem Aufruf im Archiv demos.zip gespeichert. In diesem Fall werden allerdings im Unterschied zur gzip-Implementierung im letzten Abschnitt die Originaldateien nach der Archivierung nicht gelöscht.
Der prinzipielle Ablauf beim Speichern von Daten im Zip-Format ist folgender:Im Folgenden werden diese einzelnen Schritte am Quellcode näher erläutert.
- Zunächst wird ein Exemplar von ZipOutputStream erzeugt.
- Dann werden die einzelnen Einträge des Archivs erstellt. Dabei wird zunächst ein neues ZipEntry-Exemplar erzeugt und der Methode putNextEntry(ZipEntry) als Argument übergeben.
- Nach Aufruf von putNextEntry(ZipEntry) werden jeweils die Daten für den aktuellen Eintrag in den ZipOutputStream geschrieben.
- Wenn alle Einträge eingetragen worden sind, wird das Archiv geschlossen.
Zunächst wird ein ZipOutputStream angelegt, der mit einer Datei verbunden ist, deren Name als erster Kommandozeilenparameter übergeben wird:ZipOutputStream out = new ZipOutputStream(new FileOutputStream(args[0]));Danach werden die einzelnen Einträge erstellt.
Die folgenden Schritte werden für jeden Eintrag durchgeführt. Zunächst wird ein neues Exemplar der Klasse ZipEntry erstellt:ZipEntry entry = new ZipEntry(args[i]);Der Konstruktor der Klasse ZipEntry erwartet einen Parameter vom Typ String, der den Namen des Eintrags repräsentiert. Als Name wird in diesem Fall der Name der entsprechenden Datei verwendet. Die Klasse ZipEntry speichert für jeden Eintrag mehrere Informationen, die mit den Methoden in Tabelle 10.5 abgefragt werden können.
Tabelle 10.5: Die Informationen in einem ZipEntry Methode Bedeutung String getName() Liefert den Namen des Eintrags zurück. String getComment() Liefert den Kommentar zum Eintrag zurück. long getTime() Liefert das Datum, an dem der Eintrag zuletzt verändert wurde. long getCrc() Liefert die CRC-32-Checksumme zum Eintrag zurück. long getSize() Liefert die Größe der Originaldaten zurück. long getCompressedSize() Liefert die Größe der komprimierten Daten zurück. int getMethod() Liefert den Archivierungsmodus zurück (komprimiert oder nicht komprimiert). byte[] getExtra() Liefert den Inhalt des Extra-Feldes zurück. Wenn eine der Informationen nicht bekannt ist, liefern die entsprechenden Methoden null bzw. -1 zurück, abhängig davon, ob der Rückgabewert ein Exemplar oder ein Zahlenwert ist. Für viele der in Tabelle 10.5 aufgelisteten Informationen wurden auch Methoden definiert, die das Setzen der Werte erlauben. Das get der Methoden zum Abfragen ist in diesem Fall durch set zu ersetzen und ein Parameter vom Typ des Rückgabewertes hinzuzufügen.
Für den Wert, der an setMethod(int) übergeben bzw. von getMethod() zurückgeliefert wird, wurden in der Klasse ZipEntry zwei Konstanten definiert: Ohne explizite Angabe des Archivierungsmodus wird der Modus des ZipOutputStream verwendet. Die Klasse ZipOutputStream verfügt ebenfalls über eine setMethod(int)-Methode.
Wenn ein Eintrag nicht komprimiert in einem Zip-Archiv gespeichert werden soll, müssen die Methoden setSize(long) und setCrc(long) mit geeigneten Werten aufgerufen werden. Bei Archivierung einer Datei kann die Größe des Eintrags über das zugehörige File-Objekt in Erfahrung gebracht werden. Die CRC-32-Checksumme muss über ein CRC32-Objekt ermittelt werden.
Hierzu muss zunächst ein Objekt vom Typ CRC32 erzeugt werden:CRC32 crc = new CRC32();Der Konstruktor erhält keine Parameter übergeben. In folgendem Code-Ausschnitt wird die CRC-32-Checksumme einer Datei berechnet und bei einem ZipEntry gesetzt:File f = new File(args[i]); in = new FileInputStream(f); while((read = in.read(data, 0, 1024)) != -1) crc.update(data, 0, read); in.close(); entry.setCrc(crc.getValue()); crc.reset();Alle Daten der Datei werden nacheinander der update()-Methode von CRC32 übergeben. update() ist in der Klasse CRC32 in drei Varianten definiert, jeweils mit anderen Parametern. Wurden alle Daten über update() im CRC32-Objekt gesetzt, kann man die Checksumme über die Methode getValue() abfragen. Der zurückgelieferte Wert kann dann im ZipEntry gesetzt werden. Damit ein Exemplar der CRC32-Klasse für die Berechnung mehrerer Checksummen verwendet werden kann, definiert sie die Methode reset(). Bei Aufruf von reset() wird der Zustand des CRC32-Objektes auf die Anfangsbedingungen zurückgesetzt und die Prüfsumme einer neuen Datei kann berechnet werden.
Die zip-Implementierung dieses Abschnitts kann keine nicht komprimierten Einträge erzeugen. Deshalb wird der oben beschriebene Sachverhalt nicht in der eigentlichen Implementierung verwendet. Nachdem ein neues Exemplar der Klassse ZipEntry erzeugt wurde, wird dies dem ZipOutputStream durch Aufruf von putNextEntry(ZipEntry) mitgeteilt.out.putNextEntry(entry);Das Setzen von Informationen im ZipEntry wie z. B. die Größe des Eintrags oder die Prüfsumme sollte vor Aufruf von putNextEntry(ZipEntry) erfolgen. Durch Aufruf von putNextEntry(ZipEntry) wird der aktuelle Eintrag beendet und ein neuer begonnen. Danach werden alle Daten der betreffenden Datei ausgelesen und in den ZipOutputStream geschrieben. Anschließend wird der aktuelle Eintrag durch Aufruf von closeEntry() geschlossen:out.closeEntry(); // Neuen Eintrag abschließenBei aufeinander folgenden Aufrufen von putNextEntry(ZipEntry) muss der Aufruf von closeEntry() nicht unbedingt erfolgen, da putNextEntry(ZipEntry) automatisch den aktuellen Eintrag schließt, bevor der neue begonnen wird.
Wurden auf diese Weise letztendlich alle Einträge in den ZipOutputStream geschrieben, wird die Methode close() aufgerufen und somit der Stream ebenfalls geschlossen.Material zum Beispiel
- Quelltexte:
Die Implementierung von unzip verläuft ähnlich. Das Programm erhält per Kommandozeilenparameter beliebig viele Namen von Zip-Archiven übergeben, deren Inhalt extrahiert werden soll. Für jeden übergebenen Namen wird ein ZipInputStream erzeugt, der Daten aus dem Archiv liest:ZipInputStream in = new ZipInputStream(new FileInputStream(args[i]));Durch Aufruf von getNextEntry() wird ein Verweis auf ein ZipEntry-Objekt geliefert, das den nächsten Eintrag im Archiv repräsentiert. getNextEntry() liefert null zurück, falls kein Eintrag mehr vorhanden ist. Über die Methode getMethod() wird der Archivierungsmodus des Eintrags abgefragt und abhängig vom Modus eine Meldung auf der Standardausgabe ausgegeben:if (entry.getMethod() == ZipEntry.DEFLATED) stdout.println(" Inflating: "+entry.getName()); else stdout.println(" Extracting: "+entry.getName());Die Unterscheidung der Modi beim Lesen der Daten wird automatisch vom ZipInputStream übernommen. Die Art der Speicherung dient dem Benutzer als zusätzliche Information, die aber nicht unbedingt benötigt wird.
Anschließend wird überprüft, ob bereits alle Verzeichnisse zum Anlegen der Datei existieren. Das muss gemacht werden, da mit zip im Gegensatz zu gzip auch die Verzeichnisstruktur gespeichert wird.
Hierbei müssen zwei Fälle unterschieden werden:Der erste Fall kann durch Aufruf der Methode isDirectory() vom ZipEntry-Objekt überprüft werden. Diese Methode liefert true, wenn der Eintrag nur aus einem Verzeichnis besteht. In diesem Fall werden die benötigten Verzeichnisse erstellt:
- Der Eintrag ist selbst ein Verzeichnis.
- Der Eintrag selbst ist eine Datei, aber der Pfad enthält Verzeichnisse, die noch nicht existieren.
File directory = new File(entry.getName()); directory.mkdirs();Falls das Verzeichnis bereits existiert, hat mkdirs() keine Auswirkung. Deshalb muss man nicht überprüfen, ob das Verzeichnis bereits vorhanden ist.
Besteht der Eintrag aus einer Datei, muss man herausfinden, ob im Namen des Eintrags auch Verzeichnisse enthalten sind. Hierzu wird der Name der Datei von dem Namen des Verzeichnisses mit Hilfe der Klasse StringTokenizer getrennt. Falls auch ein Verzeichnis angegeben wurde, werden auch in diesem Fall die benötigten Verzeichnisse angelegt.
Nun kann begonnen werden, die Daten aus dem ZipInputStream zu extrahieren und in eine neue Datei zu schreiben:FileOutputStream out = new FileOutputStream(entry.getName()); ... while((read = in.read(data, 0, 1024)) != -1) out.write(data, 0, read); out.close();Es ist zu beachten, dass die Methode read() der Klasse ZipOutputStream bereits -1 als Ergebnis liefert, wenn alle Daten des aktuellen Eintrags gelesen wurden. Das heißt nicht, dass alle Daten des Archivs gelesen wurden. Durch einen erneuten Aufruf von getNextEntry() wird der Stream auf den nächsten Eintrag positioniert. Danach kann man wiederum so lange read() aufrufen, bis -1 als Ergebnis geliefert wird. Erst wenn getNextEntry() null als Ergebnis zurückliefert, wurden alle Daten des Archivs verarbeitet.
Zum Auslesen eines Zip-Archivs muss nicht unbedingt die Klasse ZipInputStream verwendet werden. Der Vorteil von ZipInputStream besteht darin, dass die Klasse universell eingesetzt werden kann. Die Daten eines Zip-Archivs müssen nicht in einer Datei vorliegen, sondern könnten über jeden beliebigen Stream verfügbar sein. So könnte man z. B. über einen ZipInputStream auch Daten im Zip-Format über einen Socket lesen.
Wenn die Daten des Zip-Archivs in einer Datei vorliegen, kann man als Alternative die Klasse ZipFile verwenden. Ein ZipFile wird mit dem Namen des Zip-Archivs oder einem File-Objekt initialisiert.ZipFile zipfile = new ZipFile("demos.zip");hätte dieselbe Wirkung wie folgende Zeilen:File f = new File("demos.zip"); ZipFile zipfile = new ZipFile(f);Durch Aufruf der Methode entries() wird ein Verweis auf eine Enumeration zurückgeliefert, über die auf die einzelnen Einträge zugegriffen werden kann.
Durch Aufruf der Methode getInputStream(ZipEntry) erhält man zu einem bestimmten ZipEntry einen InputStream zurückgeliefert, über den man die Daten eines bestimmten Eintrags auslesen kann.Material zum Beispiel
- Quelltexte: