Advanced Java Services | Eine zip-Datei extrahieren |
Mit Hilfe der neuen Klassen Files, Path(s), FileSystem und FileSystems ist es möglich zip-Dateien zu extrahieren oder zu erstellen. Extrahieren und Zippen werden ab Java 7 zu (relativ) einfachen Kopiervorgängen. Beachten muß man allerdings, daß diese Kopiervorgänge u.U. zwischen verschiedenen Dateisystemen stattfinden, da eine zip-Datei ein Unix-Linux-Dateisystem darstellt. Files und Path sind von der Konzeption her für beliebige Dateisysteme angelegt.
Die Factoryklasse Paths dagegen liefert mit get() immer einen Pfad, der dem Dateisystem des nativen OS entspricht, was ja sehr oft ein Windowssystem ist, wie der folgende Ausschnitt aus dem Quellcode zeigt
public static Path get(String first, String... more) { return FileSystems.getDefault().getPath(first, more); }
Auf Windowsrechnern liefert FileSystems.getDefault() die Klasse sun.nio.fs.WindowsFileSystem
Die Methode FileSystemProvider.installedProviders() zeigt auf Windows die folgenden zwei Provider: sun.nio.fs.WindowsFileSystemProvider com.sun.nio.zipfs.ZipFileSystemProvider
Greift man mit unpassenden Pfaden auf ein Dateisystem zu, so erhät man eine ProviderMismatchException.
Ist ein Dateisystem nicht das zum Defaultsystem gehörende, so erhält man Pfade für dieses Dateisystem mit Hilfe einer Filesysteminstanz durch myFilesystem.getPath((String first, String... more).
Die Factoryklasse FileSystems kennt drei statische Methoden, die ein zip-Dateisystem liefern.
Modifier und Returntyp | Name der Methode | Beschreibung |
---|---|---|
static FileSystem | newFileSystem(Path path, ClassLoader loader) | Constructs a new FileSystem to access the contents of a file as a file system. |
static FileSystem | newFileSystem(URI uri, Map<String,?> env) | Constructs a new file system that is identified by a URI |
static FileSystem | newFileSystem(URI uri, Map<String,?> env, ClassLoader loader) | Constructs a new file system that is identified by a URI |
Die erste M ethode ist am einfachsten zu verwenden. Als ersten Parameter übergibt man einen Windowspfad, der zweite Parameter kann für einfache Fälle null sein.
Path winPath = Paths.get("./api.zip"); try( FileSystem zipFileSystem = FileSystems.newFileSystem(winPath, null);) { System.out.println(winPath + " ist zip-file"); // .\api.zip ist zip-file Path zipPath = zipFileSystem.getPath(".", "api", "java", "nio", "Buffer.html"); System.out.println("zipPath = " + zipPath.toAbsolutePath()); //zipPath = /./api/java/nio/Buffer.html System.out.println("zipPath = " + zipPath.toAbsolutePath().normalize());// zipPath = /api/java/nio/Buffer.html System.out.println(Files.exists(zipPath)); // true Path zipPath2 = zipFileSystem.getPath(".", "abra", "ka", "dabra.html"); System.out.println("zipPath2 = " + zipPath2.toAbsolutePath()); //zipPath2 = /./abra/ka/dabra.html System.out.println("zipPath2 = " + zipPath2.toAbsolutePath().normalize());// zipPath2 = /abra/ka/dabra.html System.out.println(Files.exists(zipPath2)); // false } catch(IOException ex) { ex.printStackTrace(); }
Folgendes gilt es zu beachten: Existiert die zip-Datei nicht wird eine java.nio.file.FileSystemNotFoundException geworfen.
Die zweite Methode verlangt eine URI-Instanz und eine Map. Die URI-Instanz muß genau den im Beispiel gezeigten Aufbau haben; die Map kann leer sein, aber die Übergabe von null führt zu einer NPE. Sprechender ist es jedoch die Map mit einem Eintrag zu versehen. Für das Öffnen eines Dateisystems gibt es zum Key "create" die Values "true" oder "false". "true" überschreibt allerdings eine bestehende Datei. Wählt man "false" so muß die Datei existieren, andernfalls erhält man eine FileSystemNotFoundException.
Path winPath = Paths.get("./api.zip"); Path absPath = winPath.toAbsolutePath().normalize(); // Windowspfad URI zipUri = URI.create("jar:file:///" + absPath.toString().replace('\\', '/')); // jar: ist notwendig Map<String,String> env = new HashMap<String,String>(); env.put("create", "false"); //env.put("encoding", "ISO-8859-1"); // es gibt nur diese zwei properties zu setzen try(FileSystem zipFileSystem = FileSystems.newFileSystem(zipUri, env);) { System.out.println(winPath + " ist zip-file"); } catch(IOException ex) { ex.printStackTrace(); }
Hier bietet sich das Interface FileVisitor aus dem Java 7 - Package java.nio.file an. Im folgenden Teil wird ein Filesystem angelegt und die Arbeit des Auslesens an eine Implementierung des Interfaces delegiert.
/** * Erzeugen eines zip-Filesystems mit einen Windowspfad * Auslesen des Inhalts * Elegant und einfach mit ReadZipFileVisitor = Implementierung von FileVisitor */ private static void readZipFile1() { Path winPath = Paths.get("target.zip"); Path absolute = winPath.toAbsolutePath(); Path normalized = absolute.normalize(); try(FileSystem zipFileSystem = FileSystems.newFileSystem(normalized, null)) { System.out.println("Zipped size: " + zipFileSystem.getFileStores().iterator().next().getTotalSpace() ); Iterable<Path> roots = zipFileSystem.getRootDirectories(); // there is only one Path root = roots.iterator().next(); // root = / ReadZipFileVisitor readZipFileVisitor = new ReadZipFileVisitor(); Files.walkFileTree(root, readZipFileVisitor); System.out.println("Unzipped size = " + readZipFileVisitor.getUnzippedSize() + " bytes"); } catch(IOException ex) { ex.printStackTrace(); } }
Die Implementierung des FileVisitors
public class ReadZipFileVisitor implements FileVisitor{ private long unzippedSize = 0; @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { System.out.println("<dir> " + dir); return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { long size = Files.size(file); unzippedSize += size; System.out.println(" <file>" + file + " " + size); // die angegebene Groesse ist die die originale der nicht gezippten Datei ! return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { System.out.println("<end> " + dir ); return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { return FileVisitResult.CONTINUE; } // nice to have public long getUnzippedSize() { return unzippedSize; } }
Die Klasse Files ist dateisystemunabhängig entworfen worden, dadurch ist es möglich Kopiervorgänge zwischen verschiedenen Dateisystemen zu realisieren. Man kann eine zip-Datei entpacken indem man von einem Unix-Pfad zu einem Windowspfad kopiert. Für das rekursive Durchlaufen der zip-Datei verwenden wir einen maßgeschneiderden FileVisitor. In der Methode preVisitDirectory() werden die Zielverzeichnisse angelegt, in der Methode visitFile() wird entpackt.
Der FileVisitor
import java.io.IOException; import java.nio.file.FileVisitResult; import java.nio.file.FileVisitor; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.nio.file.attribute.BasicFileAttributes; /** * @author hms */ public class ExtractZipFileVisitor implements FileVisitor<Path> { private Path destRoot; public ExtractZipFileVisitor(Path destRoot) { this.destRoot = destRoot; } @Override public FileVisitResult preVisitDirectory(Path zipDir, BasicFileAttributes attrs) throws IOException { // zipDir = unix-pfad Path destDir = Paths.get(destRoot.toString(), zipDir.toString()); // windowspfad Files.createDirectories(destDir); return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFile(Path zipFile, BasicFileAttributes attrs) throws IOException { // zipFile = unix-pfad Path dest = Paths.get(destRoot.toString(), zipFile.toString()); // windowspfad Files.copy(zipFile, dest, StandardCopyOption.REPLACE_EXISTING); return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { return FileVisitResult.CONTINUE; } }
Das Extrahieren
private static void extractZipFileUsingUnixFileSystem() { Path zipFile = Paths.get("api.zip"); Path absPath = zipFile.toAbsolutePath().normalize(); // Windowspfad URI uri = URI.create("jar:file:///" + absPath.toString().replace('\\', '/')); // jar: ist notwendig Path dest = Paths.get("."); Map<String,String> env = new HashMap<>(); env.put("create", "true"); try(FileSystem zipFileSystem = FileSystems.newFileSystem(uri, env)) { Path root = zipFileSystem.getPath("/"); Files.walkFileTree(root, new ExtractZipFileVisitor(dest)); } catch(IOException ex) { System.out.println(ex); } }
Schreibt man in die oben angesprochene Map den Eintrag "create" mit dem Value "true", so wird eine neue zip-Datei angelegt (und eine vorhandene gleichnamige Datei überschrieben)
Path winPath = Paths.get("./foo.zip"); Path absPath = winPath.toAbsolutePath().normalize(); // Windowspfad // fuer die umwandlung muss ein absoluter pfad genommen werden sonst wird c:/ genommen URI zipUri = URI.create("jar:file:///" + absPath.toString().replace('\\', '/')); Map<String,String> env = new HashMap<String,String>(); env.put("create", "true"); //env.put("encoding", "ISO-8859-1"); // es gibt nur diese zwei properties zu setzen try( FileSystem zipFileSystem = FileSystems.newFileSystem(zipUri, env);) { System.out.println(winPath + " erstellt"); } catch(IOException ex) { ex.printStackTrace(); }
Wie oben gesehen erhält man beim Anlegen einer zip-Datei ein FileSystem, über das man die zip-Datei ansprechen kann. Damit wird das Zippen wiederum zu einem Kopiervorgang, den man mit Files.copy() realisieren kann, dabei arbeitet Files.copy() so, daß die Dateien während des Kopierens komprimiert werden. Im Grunde arbeitet man genauso wie beim Entpacken. Sehr elegant geht das wieder mit einem entsprechenden FileVisitor, der diesmal CopyToZipFileVisitor heißt.
import java.io.IOException; import java.nio.file.FileSystem; import java.nio.file.FileVisitResult; import java.nio.file.FileVisitor; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.nio.file.attribute.BasicFileAttributes; public class CopyToZipFileVisitor implements FileVisitor<Path> { FileSystem zipFileSystem; // das Zielsystem public CopyToZipFileVisitor(FileSystem zipFileSystem) { this.zipFileSystem = zipFileSystem; } @Override public FileVisitResult preVisitDirectory(Path winPath, BasicFileAttributes arg1) throws IOException { Path internalTargetPath = zipFileSystem.getPath(winPath.toString()); Files.createDirectories(internalTargetPath); return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFile(Path winPath, BasicFileAttributes arg1) throws IOException { Path internalTargetPath = zipFileSystem.getPath(winPath.toString()); Files.copy(winPath, internalTargetPath, StandardCopyOption.REPLACE_EXISTING); return FileVisitResult.CONTINUE; } @Override public FileVisitResult postVisitDirectory(Path path, IOException arg1) throws IOException { return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFileFailed(Path path, IOException arg1) throws IOException { return FileVisitResult.CONTINUE; } }
Und so wird gezippt
private static void createAndCopyToZipFileUsingUnixFileSystem() { Path source = Paths.get("api"); Path zipFile = Paths.get("api.zip"); Path absPath = zipFile.toAbsolutePath().normalize(); // Windowspfad URI zipUri = URI.create("jar:file:///" + absPath.toString().replace('\\', '/')); // jar: ist notwendig // evtl. Files.exists() verwenden und untersuchen, ob die Datei schon existiert Map<String,String> env = new HashMap<>(); env.put("create", "true"); try(FileSystem zipFileSystem = FileSystems.newFileSystem(zipUri, env);) { System.out.println(zipFile + " angelegt"); Files.walkFileTree(source, new CopyToZipFileVisitor(zipFileSystem) ); System.out.println("dateien gezippt"); } catch(IOException ex) { ex.printStackTrace(); } }
Man sieht, daß die beiden Filevisitoren im Wesentlichen gleich arbeiten, mit kleinen Änderungen kann man einen FileVisitor schreiben, der in beide Richtungen arbeiten kann.
Übung
Schreiben Sie einen FileVisitor, der den Inhalt einer Zip-Datei auflistet.