Tworzenie archiwum Zip

Koziołek

1 Interfejs <samp>Archive</samp>
2 Interfejs <samp>ZipArchive</samp>
3 Implementacja prostej kompresji ZIP
4 Implementacja kompresji ZIP z sumą kontrolną
5 Refaktoryzacja
6 Fabryka ZIP
7 Rozpakowanie
8 Podsumowanie

Na forum pojawia się co pewien czas pytanie o to jak archiwizować pliki za pomocą Zip i Javy. Ten artykuł demonstruje podstawową metodę archiwizacji.

Interfejs Archive

Jako, że możemy użyć kilku różnych metod kompresji zatem warto obudować nasze rozwiązanie za pomocą odpowiedniego interfejsu. Jest to prosty interfejs posiadający tylko jedną metodę. Metoda przyjmuje jako argumenty plik źródłowy i plik wynikowy.

package pl.koziolekweb.programmers.zip;

import java.io.File;
import java.io.IOException;

/**
 * Interfejs klasy archiwizującej.
 * 
 * @author koziolek
 * 
 */
public interface Archiver {

	/**
	 * Tworzy archiwum z pliku lub folderu podanego w parametrze
	 * <code>source&lt;/code> w pliku podanym w parametrze <code>target&lt;/code>.
	 * 
	 * @param source
	 *            plik/folder źródłowy.
	 * @param target
	 *            miejsce powstania archiwum.
	 * @throws IOException
	 *             zwracany jeżeli pojawi się problem z plikami. Zazwyczaj
	 *             chodzi o dostęp do pliku.
	 */
	void archive(File source, File target) throws IOException;
}

Interfejs ZipArchive

Interfejs Archive pozwala na stosowanie wielu różnych algorytmów. Dzięki temu jak później się przekonamy, istnieje możliwość dostarczenia różnych implementacji w oparciu o wzorzec fabryki. Nie jest to jednak rozwiązanie na tyle elastyczne by zapewnić nam dostęp specyficznych właściwości dla różnych typów kompresji. W tym celu stworzymy interfejs ZipArchive. Będzie on specyfikował pewne właściwości unikalne dla archiwum ZIP.

package pl.koziolekweb.programmers.zip;

/**
 * Interfejs dla archiwmum ZIP.
 * 
 * @author koziolek
 * 
 */
public interface ZipArchiver extends Archiver {

	/**
	 * Poziom kompresji.
	 * 
	 * @author koziolek
	 * 
	 */
	public enum LEVEL {
		_0(0), _1(1), _2(2), _3(3), _4(4), _5(5), _6(6), _7(7), _8(8), _9(9);

		private LEVEL(int lvl) {
			this.lvl = lvl;
		}

		int lvl;
	}

	/**
	 * Metoda pozwala na dodanie komentarza do archiwum.
	 * 
	 * @param message
	 *            komentarz.
	 */
	public void setComment(String message);
}

Jak widać mamy tu możliwość dodania wiadomości dla osoby otwierającej archiwum oraz pozwalamy na określenie poziomu kompresji. Jak za chwilę się przekonamy poziom kompresji jest podawany jako zwykła liczba typu int. Moglibyśmy pozwolić na wykonanie tego typu operacji. Takie podejście jest jednak niewłaściwe ponieważ wymaga od nas tworzenia dodatkowego kodu, który sprawdzałby czy podana przez użytkownika liczba znajduje się w zakresie. Kod taki wymagałby dodatkowego testowania, udokumentowania i utrzymania. Enum pozwala na weryfikację poprawności kodu na poziomie kompilatora bez konieczności tworzenia dodatkowego kodu. Takie podejście nazywamy lenistwem i jest to dobre.

Implementacja prostej kompresji ZIP

Mamy już interfejs zatem możemy przystąpić do jego implementacji. Nasza klasa będzie pozwalała na wybór stopnia kompresji (0-9) albo użycie domyślnego stopnia (5). Powinna też obsługiwać zagnieżdżenia, czyli katalogi. Poniższy kod przedstawia tą właśnie klasę:

package pl.koziolekweb.programmers.zip;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * 
 * Klasa tworzy archiwum za pomocą algorytmu ZIP.
 * 
 * @author koziolek
 * 
 */
public class ArchiverSimpleZIPImpl implements ZipArchiver {
	
	private static final int BUFFER = 2048;

	private FileOutputStream dest;

	private ZipOutputStream out;

	private BufferedInputStream entryStream;

	private String basePath;

	private LEVEL compresionLevel;

	private String message;

	/**
	 * Używany jest domyślny poziom kompresji (5)
	 */
	public ArchiverSimpleZIPImpl() {
		this(LEVEL._5);
	}

	/**
	 * Używany jest wskazany poziom kompresji.
	 * 
	 * @param compresionLevel
	 *            poziom kompresji.
	 */
	public ArchiverSimpleZIPImpl(LEVEL compresionLevel) {
		this.compresionLevel = compresionLevel;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see pl.koziolekweb.programmers.zip.Archiver#archive(File, File)
	 */
	@Override
	public void archive(File source, File target) throws IOException {
		if (!target.exists()) {
			target.createNewFile();
		}
		basePath = source.getAbsolutePath();

		dest = new FileOutputStream(target);
		out = new ZipOutputStream(new BufferedOutputStream(dest));
		out.setLevel(compresionLevel.lvl);
		setMethod();
		if (message != null) {
			out.setComment(message);
		}

		if (source.isDirectory()) {
			archiveDir(source);
		} else {
			archiveFile(source);
		}
		out.close();
	}

	/**
	 * Ustawia metodę w zależności od poziomu kompresji.
	 */
	private void setMethod() {
		if (this.compresionLevel != LEVEL._0) {
			out.setMethod(ZipOutputStream.DEFLATED);
		} else {
			out.setMethod(ZipOutputStream.STORED);
		}
	}

	/**
	 * Przetwarza rekurencyjnie folder jeżeli próbujemy dodać go do archiwum.
	 * Jeżeli przekażemy do metody zwykły plik to zostanie on zignorowany.
	 * 
	 * @param file
	 *            folder.
	 * @throws IOException
	 */
	private void archiveDir(File file) throws IOException {
		if (file.isFile()) {
			System.out.println("Ignoruję zwykły plik: " + file.getAbsolutePath());
		}
		for (File f : file.listFiles()) {
			if (f.isDirectory()) {
				archiveDir(f);
			} else {
				archiveFile(f);
			}
		}
	}

	/**
	 * Dodaje wskazany plik do archiwum.
	 * 
	 * @param file
	 *            plik do dodania.
	 * @throws IOException
	 */
	private void archiveFile(File file) throws IOException {
		String targetFilePath = file.getAbsolutePath().replace(basePath, "");
		byte data[] = new byte[BUFFER];
		
		FileInputStream fi = new FileInputStream(file);
		entryStream = new BufferedInputStream(fi, BUFFER);
		ZipEntry entry = new ZipEntry(targetFilePath);
		out.putNextEntry(entry);
		int count;
		while ((count = entryStream.read(data, 0, BUFFER)) != -1) {
			out.write(data, 0, count);
		}
		entryStream.close();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * pl.koziolekweb.programmers.zip.ZipArchiver#setMessage(java.lang.String)
	 */
	@Override
	public void setComment(String message) {
		this.message = message;
	}
}

Nie będę dokładnie analizował kodu, zostawiam to czytelnikowi, ale skupię się na najważniejszych jego elementach.
Klasa posiada dwa konstruktory. Pierwszy, bezparametrowy wywołuje drugi z domyślnym poziomem kompresji. Jest to zastosowanie w praktyce wzorca Łańcuch konstruktorów. Pozwala nam to na bezpieczne modyfikowanie sposobu tworzenia obiektów klasy bez konieczności powtarzania kodu.
Metoda archive nie jest bezpośrednio odpowiedzialna za archiwizację, ale przygotowuje pole dla metod archiveDir i archiveFile. Pierwsza z nich pozwala na obsługę zagnieżdżeń katalogów poprzez odpowiednie delegowanie obsługi odpowiednich plików/katalogów do siebie samej lub do drugiej metody. archiveFile wykonuje właściwą pracę. Pierwsza linijka usuwa fragment ścieżki do pliku pozostawiając samą ścieżkę względną. Jeżeli tego byśmy nie zrobili to po zapakowaniu pliku c:\test\test.txt i jego rozpakowaniu do katalogu d:\test otrzymalibyśmy d:\test\C_\test\test.txt. W systemach unixowych zamiast C_ pojawiła by się pełna ścieżka do pliku np /home/koziolek/home/mysz/test/test.txt. Następnie metoda tworzy odpowiednie strumienie i encję ZIP zapisuje plik i kończy działanie.
Przejdziemy teraz do kompresji ze sprawdzeniem sum kontrolnych.

Implementacja kompresji ZIP z sumą kontrolną

Implementacja ta nie różni się znacząco od prostej implementacji ZIP. Jedyna zmiana to dodanie pośredniczącego elementu w postaci obiektu klasy CheckedOutputStream, który wylicza sumy za pomocą algorytmu Adler32.

package pl.koziolekweb.programmers.zip;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.Adler32;
import java.util.zip.CheckedOutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * 
 * Klasa tworzy archiwum za pomocą algorytmu ZIP.
 * 
 * @author koziolek
 * 
 */
public class ArchiverChecksumZIPImpl implements ZipArchiver {

	private static final int BUFFER = 2048;

	private FileOutputStream dest;

	private ZipOutputStream out;

	private BufferedInputStream entryStream;

	private String basePath;

	private LEVEL compresionLevel;

	private String message;

	private CheckedOutputStream checksumStream;

	/**
	 * Używany jest domyślny poziom kompresji (5)
	 */
	public ArchiverChecksumZIPImpl() {
		this(LEVEL._5);
	}

	/**
	 * Używany jest wskazany poziom kompresji.
	 * 
	 * @param compresionLevel
	 *            poziom kompresji.
	 */
	public ArchiverChecksumZIPImpl(LEVEL compresionLevel) {
		this.compresionLevel = compresionLevel;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see pl.koziolekweb.programmers.zip.Archiver#archive(File, File)
	 */
	@Override
	public void archive(File source, File target) throws IOException {
		if (!target.exists()) {
			target.createNewFile();
		}
		basePath = source.getAbsolutePath();

		dest = new FileOutputStream(target);
		checksumStream = new CheckedOutputStream(dest, new Adler32());
		out = new ZipOutputStream(new BufferedOutputStream(checksumStream));
		out.setLevel(compresionLevel.lvl);
		setMethod();
		if (message != null) {
			out.setComment(message);
		}

		if (source.isDirectory()) {
			archiveDir(source);
		} else {
			archiveFile(source);
		}
		out.close();
	}

	/**
	 * Ustawia metodę w zależności od poziomu kompresji.
	 */
	private void setMethod() {
		if (this.compresionLevel != LEVEL._0) {
			out.setMethod(ZipOutputStream.DEFLATED);
		} else {
			out.setMethod(ZipOutputStream.STORED);
		}
	}

	/**
	 * Przetwarza rekurencyjnie folder jeżeli próbujemy dodać go do archiwum.
	 * Jeżeli przekażemy do metody zwykły plik to zostanie on zignorowany.
	 * 
	 * @param file
	 *            folder.
	 * @throws IOException
	 */
	private void archiveDir(File file) throws IOException {
		if (file.isFile()) {
			System.out.println("Ignoruję zwykły plik: " + file.getAbsolutePath());
		}
		for (File f : file.listFiles()) {
			if (f.isDirectory()) {
				archiveDir(f);
			} else {
				archiveFile(f);
			}
		}
	}

	/**
	 * Dodaje wskazany plik do archiwum.
	 * 
	 * @param file
	 *            plik do dodania.
	 * @throws IOException
	 */
	private void archiveFile(File file) throws IOException {
		String targetFilePath = file.getAbsolutePath().replace(basePath, "");
		byte data[] = new byte[BUFFER];

		FileInputStream fi = new FileInputStream(file);
		entryStream = new BufferedInputStream(fi, BUFFER);
		ZipEntry entry = new ZipEntry(targetFilePath);
		out.putNextEntry(entry);
		int count;
		while ((count = entryStream.read(data, 0, BUFFER)) != -1) {
			out.write(data, 0, count);
		}
		entryStream.close();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * pl.koziolekweb.programmers.zip.ZipArchiver#setMessage(java.lang.String)
	 */
	@Override
	public void setComment(String message) {
		this.message = message;
	}
}

Refaktoryzacja

Jak łatwo zauważyć klasy ArchiverSimpleZIPImpl i ArchiverChecksumZIPImpl nie różnią się zbytnio. Należało by przeprowadzić refaktoryzację i wyciągnąć wspólne elementy do abstrakcyjnej nadklasy. Wynik tej operacji powinien wyglądać tak:

/**
 * 
 */
package pl.koziolekweb.programmers.zip;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * Abstrakcyjna klasa do obsługi archiwizacji za pomocą ZIP. Klasy rozszerzające
 * muszą nadpisać metodę {@link #makeStreams(File)}.
 * 
 * @author koziolek
 * 
 */
public abstract class AchiverAbstractZIPImpl implements ZipArchiver {

	protected static final int BUFFER = 2048;

	protected FileOutputStream dest;

	protected ZipOutputStream out;

	protected BufferedInputStream entryStream;

	protected String basePath;

	protected LEVEL compresionLevel;

	protected String message;

	/*
	 * (non-Javadoc)
	 * 
	 * @see pl.koziolekweb.programmers.zip.Archiver#archive(File, File)
	 */
	@Override
	public void archive(File source, File target) throws IOException {
		if (!target.exists()) {
			target.createNewFile();
		}
		basePath = source.getAbsolutePath();

		makeStreams(target);
		setMethod();
		if (message != null) {
			out.setComment(message);
		}

		if (source.isDirectory()) {
			archiveDir(source);
		} else {
			archiveFile(source);
		}
		out.close();
	}

	/**
	 * Konfiguruje strumienie.
	 *  
	 * @param target plik archiwum.
	 * @throws FileNotFoundException jeżeli nie istnieje plik wynikowy.
	 */
	protected abstract void makeStreams(File target) throws FileNotFoundException;
	
	/**
	 * Ustawia metodę w zależności od poziomu kompresji.
	 */
	private void setMethod() {
		if (this.compresionLevel != LEVEL._0) {
			out.setMethod(ZipOutputStream.DEFLATED);
		} else {
			out.setMethod(ZipOutputStream.STORED);
		}
	}

	/**
	 * Przetwarza rekurencyjnie folder jeżeli próbujemy dodać go do archiwum.
	 * Jeżeli przekażemy do metody zwykły plik to zostanie on zignorowany.
	 * 
	 * @param file
	 *            folder.
	 * @throws IOException
	 */
	private void archiveDir(File file) throws IOException {
		if (file.isFile()) {
			System.out.println("Ignoruję zwykły plik: " + file.getAbsolutePath());
		}
		for (File f : file.listFiles()) {
			if (f.isDirectory()) {
				archiveDir(f);
			} else {
				archiveFile(f);
			}
		}
	}

	/**
	 * Dodaje wskazany plik do archiwum.
	 * 
	 * @param file
	 *            plik do dodania.
	 * @throws IOException
	 */
	private void archiveFile(File file) throws IOException {
		String targetFilePath = file.getAbsolutePath().replace(basePath, "");
		byte data[] = new byte[BUFFER];

		FileInputStream fi = new FileInputStream(file);
		entryStream = new BufferedInputStream(fi, BUFFER);
		ZipEntry entry = new ZipEntry(targetFilePath);
		out.putNextEntry(entry);
		int count;
		while ((count = entryStream.read(data, 0, BUFFER)) != -1) {
			out.write(data, 0, count);
		}
		entryStream.close();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * pl.koziolekweb.programmers.zip.ZipArchiver#setMessage(java.lang.String)
	 */
	@Override
	public void setComment(String message) {
		this.message = message;
	}
}

Klasy ArchiverSimpleZIPImpl i ArchiverChecksumZIPImpl wyglądają następująco:

package pl.koziolekweb.programmers.zip;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.util.zip.ZipOutputStream;

/**
 * 
 * Klasa tworzy archiwum za pomocą algorytmu ZIP.
 * 
 * @author koziolek
 * 
 */
final class ArchiverSimpleZIPImpl extends AchiverAbstractZIPImpl {
	
	/**
	 * Używany jest domyślny poziom kompresji (5)
	 */
	ArchiverSimpleZIPImpl() {
		this(LEVEL._5);
	}

	/**
	 * Używany jest wskazany poziom kompresji.
	 * 
	 * @param compresionLevel
	 *            poziom kompresji.
	 */
	ArchiverSimpleZIPImpl(LEVEL compresionLevel) {
		this.compresionLevel = compresionLevel;
	}

	/**
	 * Konfiguruje strumienie. Nie tworzy pośrednikow.
	 *  
	 * @param target plik archiwum.
	 * @throws FileNotFoundException jeżeli nie istnieje plik wynikowy.
	 */
	@Override
	protected void makeStreams(File target) throws FileNotFoundException{
		dest = new FileOutputStream(target);
		out = new ZipOutputStream(new BufferedOutputStream(dest));
		out.setLevel(compresionLevel.lvl);
	}
	
}
package pl.koziolekweb.programmers.zip;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.Adler32;
import java.util.zip.CheckedOutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * 
 * Klasa tworzy archiwum za pomocą algorytmu ZIP.
 * 
 * @author koziolek
 * 
 */
final class ArchiverChecksumZIPImpl extends AchiverAbstractZIPImpl {

	private CheckedOutputStream checksumStream;

	/**
	 * Używany jest domyślny poziom kompresji (5)
	 */
	ArchiverChecksumZIPImpl() {
		this(LEVEL._5);
	}

	/**
	 * Używany jest wskazany poziom kompresji.
	 * 
	 * @param compresionLevel
	 *            poziom kompresji.
	 */
	ArchiverChecksumZIPImpl(LEVEL compresionLevel) {
		this.compresionLevel = compresionLevel;
	}

	/**
	 * Metoda ustawia pośredni strumien klasy {@link CheckedOutputStream}.
	 *  
	 * @param target plik wynikowy.
	 * @throws FileNotFoundException jeżeli nie można znaleźć pliku wynikowego
	 */
	protected void makeStreams(File target) throws FileNotFoundException {
		dest = new FileOutputStream(target);
		checksumStream = new CheckedOutputStream(dest, new Adler32());
		out = new ZipOutputStream(new BufferedOutputStream(checksumStream));
		out.setLevel(compresionLevel.lvl);
	}

}

Jak widać kod jest już stosunkowo przejrzysty. Przy okazji zmieniliśmy widoczność klas na domyślną co pozwoli nam na przygotowanie się do kolejnego kroku.

Fabryka ZIP

Ostatnim elementem, który omówię jest przygotowanie naszej biblioteki do użytku dla innych programistów. Najlepsze biblioteki to taki, które pozwalają na swobodny dostęp i zmianę, ale jednocześnie ukrywają implementację. W tym celu zmieniliśmy już widoczność naszych klas oraz całość obudowaliśmy interfejsami. Takie podejście zapewnia zarówno elastyczność jak i bezpieczeństwo. Klient nie ma dostępu do "mięsa", dzięki czemu nie będzie używał pojedyńczych metod z klas i nie zrobi sobie krzywdy.
Pozostaje jeszcze problem udostępnienia implementacji. W tym celu stworzymy klasę ArchiverFactory. Będzie ona dostarczać implementacji różnych interfejsów dziedziczących po Archiver. Będzie też posiadała wyspecjalizowane metody, które pozwolą na tworzenie implementacji opartych o różne pod interfejsy.

package pl.koziolekweb.programmers.zip;

import pl.koziolekweb.programmers.zip.ZipArchiver.LEVEL;

/**
 * Dostarcza różnych archiwizatorów.
 * 
 * @author koziolek
 * 
 */
public class ArchiverFactory {

	/**
	 * Dostępne rodzaje archiwizatorów.
	 * 
	 * @author koziolek
	 * 
	 */
	public enum TYPE {
		SIMPLE_ZIP, CHECKSUM_ZIP;
	}

	public enum ZIP_TYPE {
		SIMPLE_ZIP, CHECKSUM_ZIP;
	}

	/**
	 * Dostracza domyślną implementację wskazanego archiwizatora.
	 * 
	 * @param type
	 *            rodzaj archiwizatora.
	 * @return archiwizator.
	 */
	public static Archiver getArchiver(TYPE type) {
		switch (type) {
		case SIMPLE_ZIP:
			return new ArchiverSimpleZIPImpl();
		case CHECKSUM_ZIP:
			return new ArchiverChecksumZIPImpl();
		default:
			throw new Error("Coś się stało i to bardzo złego");
		}
	}

	/**
	 * Dostarcza różne implementacje kompresji ZIP.
	 * 
	 * @param type rodzaj kompresji ZIP.
	 * @param level poziom kompresji.
	 * @return archiwizator wykorzystujący algorytm ZIP.
	 */
	public static ZipArchiver getZipArchiver(ZIP_TYPE type, LEVEL level) {
		switch (type) {
		case SIMPLE_ZIP:
			return new ArchiverSimpleZIPImpl();
		case CHECKSUM_ZIP:
			return new ArchiverChecksumZIPImpl();
		default:
			throw new Error("Coś się stało i to bardzo złego");
		}
	}
}

Ze względu na to, że standardowa biblioteka java dostarcza tylko kompresji ZIP zachęcam czytelnika do rozbudowy tego kodu o dodatkowe rodzaje algorytmów.

Rozpakowanie

Mamy już gotowe klasy odpowiedzialne za kompresję plików. Należy jeszcze przygotować klasę narzędziową, która rozpakuje nam archiwum.
Tradycyjnie rozpoczniemy od przygotowania interfejsu, który pozwoli nam na późniejsze rozszerzanie biblioteki o kolejne algorytmy:

package pl.koziolekweb.programmers.zip;

import java.io.File;
import java.io.IOException;

/**
 * Interfejz dla algorytmów rozpakowujacych.
 * 
 * @author koziolek
 * 
 */
public interface Unarchiver {

	/**
	 * Rozpakowuje <samp>source</samp> do <samp>target</samp>.
	 * 
	 * @param source
	 *            archiwum.
	 * @param target
	 *            katalog doceowy.
	 * @throws IOException
	 *             jeżeli archiwum lub folder docelowy są niedostępne.
	 */
	public void unarchive(File source, File target) throws IOException;
}

Następnie wystarczy zaimplementować klasę rozpakowującą pliki ZIP.

package pl.koziolekweb.programmers.zip;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

/**
 * Klasa rozpakowuje pliki ZIP.
 * 
 * @author koziolek
 * 
 */
public final class UnarchiverZip implements Unarchiver {

	private static final int BUFFER = 2048;

	@Override
	public void unarchive(File source, File target) throws IOException {
		BufferedOutputStream dest = new BufferedOutputStream(new FileOutputStream(target));
		FileInputStream fis = new FileInputStream(source);
		ZipInputStream zis = new ZipInputStream(new BufferedInputStream(fis));
		ZipEntry entry;
		while ((entry = zis.getNextEntry()) != null) {
			int count;
			byte data[] = new byte[BUFFER];
			if (entry.isDirectory()) {
				File f = new File(target.getAbsolutePath() + entry.getName());
				if(!f.exists()){
					f.mkdirs();
				}
				continue;				
			}
			FileOutputStream fos = new FileOutputStream(entry.getName());
			dest = new BufferedOutputStream(fos, BUFFER);
			while ((count = zis.read(data, 0, BUFFER)) != -1) {
				dest.write(data, 0, count);
			}
			dest.flush();
			dest.close();
		}
		zis.close();
	}
}

Jak widać kod rozpakowujacy jest znacznie prostszy.

Podsumowanie

W artykule przybliżyłem czytelnikowi sposób obsługi archiwum ZIP w javie. Przy okazji stworzyłem szkielekt biblioteki, którą czytelnik może wypełnić implementacjami innych algorytmów.

Komplet biblioteka, źródła i javadoc:
ZipTutorial.zip

7 komentarzy

Niestety, pomimo wielu różnych prób, nie mogę tego zmusić do działania.
Sprawdziłem w NetBeans, sprawdziłem na innym komputerze.
Nie sprawdziłem na 1.5 bo nie mogę znaleźć.
Nie działa, wywala się na dostępie do katalogu przy użyciu FileOutputStream()...

Echhh zawsze wiedziałem, że na Javę mogę "liczyć"...
W końcu znalazłem w dokumentacji javy FileOutputStream:
"If the file exists but is a directory rather than a regular file, does not exist but cannot be created, or cannot be opened for any other reason then a FileNotFoundException is thrown."

Tylko co dalej... :(

XP i java 1.6u14 (i eclipse 3.5).

Kombinuje, kombinuje i nic z tego nie wychodzi. Jak katalog 'bbb' istnieje, to nie mogę się do niego dostać.

java.io.FileNotFoundException: D:\Java-workspaces\workspace-eclipse\zip\src\bbb (Odmowa dostępu)
        at java.io.FileOutputStream.open(Native Method)
        at java.io.FileOutputStream.<init>(Unknown Source)
        at java.io.FileOutputStream.<init>(Unknown Source)
        at UnarchiverZip.unarchive(UnarchiverZip.java:23)
        at zip.main(zip.java:127)

Jak nie istnieje to:

  1. zaklada katalog "bbbaaaa" (czyli katalog target 'bbb' połączony nazwą z tym z zipa, czyli 'aaaa'. To oczywiście się wywala (no to rozumiem):
java.io.FileNotFoundException: aaaa\S1.xml (System nie może odnaleźć określonej ścieżki)
        at java.io.FileOutputStream.open(Native Method)
        at java.io.FileOutputStream.<init>(Unknown Source)
        at java.io.FileOutputStream.<init>(Unknown Source)
        at UnarchiverZip.unarchive(UnarchiverZip.java:39)
        at zip.main(zip.java:127)
  1. Więc zmieniłem w unarchiverZip.java:31:
    File f = new File(target.getAbsolutePath() + '\' + entry.getName();
    ale niewiele pomogło.
java.io.FileNotFoundException: aaaa\S1.xml (System nie może odnaleźć określonej ścieżki)
        at java.io.FileOutputStream.open(Native Method)
        at java.io.FileOutputStream.<init>(Unknown Source)
        at java.io.FileOutputStream.<init>(Unknown Source)
        at UnarchiverZip.unarchive(UnarchiverZip.java:39)
        at zip.main(zip.java:127)

i zerowy plik 'bbb', brak założonego katalogu 'bbb'.

Zip ma w sobie jeden katalog i dwa pliki w nim:
aaaa/S1.xml
aaaa/S2.xml

No właśnie problem w tym, że tak na początku próbowałem (to: "//t.mkdir();" i próbowałem też "t.mkdirs").
Niestety kiszka, po dodaniu Twojej propozycji z .exists():

java.io.FileNotFoundException: D:\Java-workspaces\workspace-eclipse\zip\src\bbb (Odmowa dostępu)
	at java.io.FileOutputStream.open(Native Method)
	at java.io.FileOutputStream.<init>(Unknown Source)
	at java.io.FileOutputStream.<init>(Unknown Source)
	at UnarchiverZip.unarchive(UnarchiverZip.java:23)
	at zip.main(zip.java:128)

Katalog zakłada.
Ale to jest podobny efekt jak opisałem poniżej, gdy katalog już był, albo gdy podałem bieżący ".".

Hm... Ok, skoro nie istnieje katalog do którego chcesz rozpakować plik zip to się wywali. Musisz zrobić tak:

File targetFile = new File(target);
if(!targetFile.exist()){
   targetFile.mkdirs(); // tworzy całą ścieżkę z katalogami pośrednimi
}
unarchive(myZip; targetFile);

Szybkie pytanie Vista?

Nic z tego nie rozumiem :(
Dlaczego rzuca się o

File t = new File(".");
java.io.FileNotFoundException: . (Odmowa dostępu)
	at java.io.FileOutputStream.open(Native Method)
	at java.io.FileOutputStream.<init>(Unknown Source)
	at java.io.FileOutputStream.<init>(Unknown Source)
	at UnarchiverZip.unarchive(UnarchiverZip.java:23)
	at zip.main(zip.java:125)

a ta 23 linia w UnarchiverZip.java to:

new FileOutputStream(target));

Przede wszystkim - dzięki wielkie za ten przykład.
Tak sie właśnie miotałem w temacie JAVA<->ZIP, dzięki temu przykładowi stało się to wszystko łatwiejsze. Mam jednak problem z użyciem UnarchiveZip. Nie wiedzieć czemu obiekt File target nie zachowuje się tak jakbym chciał (czyli że jest ścieżką gdzie ma być rozpakowany zip).
Poszedłem po najmniejszej linii oporu, ale coś sknociłem:

		UnarchiverZip z = new UnarchiverZip();
		File s = new File("D:\\Java-workspaces\\workspace-eclipse\\zip\\src\\S.zip");
		File t = new File("D:\\Java-workspaces\\workspace-eclipse\\zip\\src\\as"); //ma założyć taki katalog i do niego rozpakować zawartosc zip'a
		//t.mkdir(); //proby
		try {
			z.unarchive(s, t);
		}catch(IOException e){
			e.printStackTrace();	
		}

W takiej formie, rozpakowuwuje pliki do "D:\Java-workspaces\workspace-eclipse\zip" (czemu to nie wiem), równocześnie w "D:\Java-workspaces\workspace-eclipse\zip\src\as" zakłada 0 plik 'as'.

Ale jak założę taki katalog t.mkdir to mam wyjątek:

java.io.FileNotFoundException: D:\Java-workspaces\workspace-eclipse\zip\src\as (Odmowa dostępu)
	at java.io.FileOutputStream.open(Native Method)
	at java.io.FileOutputStream.<init>(Unknown Source)
	at java.io.FileOutputStream.<init>(Unknown Source)
	at UnarchiverZip.unarchive(UnarchiverZip.java:23)
	at zip.main(zip.java:125)

no i jestem głupi teraz... :)