Advanced   Java   Services Eine zip-Datei extrahieren


Lesen und Schreiben von zip-Dateien mit Java 7

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).


Eine zip-Datei öffnen

Die Factoryklasse FileSystems kennt drei statische Methoden, die ein zip-Dateisystem liefern.

Modifier und ReturntypName der MethodeBeschreibung
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


Eine zip-Datei öffnen mit FileSystems.newFileSystem(Path path, ClassLoader loader)

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.


Eine zip-Datei öffnen mit FileSystems.newFileSystem(URI uri, Map<String,?> env)

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();
}

Lesen einer zip-Datei

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;
  }
}

Entpacken einer zip-Datei durch Kopieren mit Hilfe der Klasse Files

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);
   }
}

Eine neue (leere) zip-Datei anlegen mit FileSystems.newFileSystem(URI uri, Map<String,?> env)

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();
}

Dateien zippen mit Java 7

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


Übung

Schreiben Sie einen FileVisitor, der den Inhalt einer Zip-Datei auflistet.