package org.geotoolkit.nio;
import org.geotoolkit.lang.Static;
import java.io.*;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.*;
import java.util.*;
import java.util.zip.*;
/**
* Utility methods related to I/O Zip operations.
*
* @author Quentin Boileau (Geomatys)
*/
public class ZipUtilities extends Static {
/**
* The default buffer size for copy operations.
*/
private static final int BUFFER_SIZE = 8192;
private ZipUtilities() {
}
/**
* Returns a zip file system
*
* @param zipPath to construct the file system from
* @param create true if the zip file should be created
* @return a zip file system
* @throws IOException
*/
private static FileSystem createZipFileSystem(Path zipPath, boolean create)
throws IOException {
// convert the filename to a URI
final URI uri = URI.create("jar:file:" + zipPath.toAbsolutePath().toString());
final Map<String, String> env = new HashMap<>();
if (create) {
env.put("create", "true");
}
return FileSystems.newFileSystem(uri, env);
}
/**
* Zip resources list into zip file using Java NIO API.
*
* @param zipPath
* @param resouces
* @throws IOException
*/
public static void zipNIO(Path zipPath, final Path... resouces) throws IOException {
Files.deleteIfExists(zipPath);
try (FileSystem zipFs = createZipFileSystem(zipPath, true)) {
final Path zipRoot = zipFs.getPath("/");
for (Path resouce : resouces) {
final Path nResouce = resouce.normalize();
if (Files.isRegularFile(nResouce, LinkOption.NOFOLLOW_LINKS)) {
final Path dest = zipRoot.resolve(nResouce.getFileName().toString());
Files.copy(nResouce, dest, StandardCopyOption.REPLACE_EXISTING);
} else {
Files.walkFileTree(nResouce, new CopyFileVisitor(zipRoot, StandardCopyOption.REPLACE_EXISTING));
}
}
}
}
/**
* Unzip zipped file into target path using Java NIO API.
*
* @param zipPath
* @param target
* @param withRootDirectory create folder into target path named after zip filename
* @throws IOException
*/
public static void unzipNIO(Path zipPath, final Path target, boolean withRootDirectory) throws IOException {
if(Files.notExists(target)) {
Files.createDirectories(target);
}
final Path deflateDirectory;
if (withRootDirectory) {
final String folderName = IOUtilities.filenameWithoutExtension(zipPath);
deflateDirectory = Files.createDirectories(target.resolve(folderName));
} else {
deflateDirectory = target;
}
try (FileSystem zipFS = createZipFileSystem(zipPath, false)) {
final Path root = zipFS.getPath("/");
Files.walkFileTree(root, new CopyFileVisitor(deflateDirectory, StandardCopyOption.REPLACE_EXISTING));
}
}
/**
* <p>This method allows to put resources into zip archive specified resource,
* without compression</p>
*
* @param zip The resource which files will be archived into. This argument must be
* instance of File, String (representing a path), or OutputStream. Cannot be null.
* @param checksum Checksum object (instance of Alder32 or CRC32).
* @param resources The files to compress. Tese objects can be File instances or
* String representing files paths, URL, URI or InputStream. Cannot be null.
* @throws IOException
*/
public static void zip(final Path zip, final Checksum checksum, final Path... resources) throws IOException {
zip(zip, ZipOutputStream.STORED, 0, checksum, resources);
}
/**
* <p>This method allows to put resources into zip archive specified resource.</p>
*
* @param zip The resource which files will be archived into. This argument must be
* instance of File, String (representing a path), or OutputStream. Cannot be null.
* @param method The compression method is a static int constant from ZipOutputSteam with
* two theorical possible values :
* <ul>
* <li>{@link ZipOutputStream#DEFLATED} to compress archive.</li>
* <li>{@link ZipOutputStream#STORED} to let the archive uncompressed (unsupported).</li>
* </ul>
* @param level The compression level is an integer between 0 (not compressed) to 9 (best compression).
* @param checksum Checksum object (instance of Alder32 or CRC32).
* @param resources The files to compress. Tese objects can be File instances or
* String representing files paths, URL, URI or InputStream. Cannot be null.
* @throws IOException
*/
public static void zip(final Path zip, final int method, final int level, final Checksum checksum, final Path... resources)
throws IOException {
try (OutputStream outStream = Files.newOutputStream(zip)) {
if (checksum != null) {
try (CheckedOutputStream cos = new CheckedOutputStream(outStream, checksum)) {
try (BufferedOutputStream buf = new BufferedOutputStream(cos)) {
zipCore(method, level, buf, resources);
}
}
} else {
try (BufferedOutputStream buf = new BufferedOutputStream(outStream)) {
zipCore(method, level, buf, resources);
}
}
}
}
/**
* Intermediate operation during ip creation
* @param method
* @param level
* @param buf
* @param resources
* @throws IOException
*/
private static void zipCore(int method, int level, BufferedOutputStream buf, Path[] resources) throws IOException {
try (final ZipOutputStream zout = new ZipOutputStream(buf)) {
zout.setMethod(method);
zout.setLevel(level);
zipCore(zout, method, level, "", Arrays.asList(resources).iterator());
}
}
/**
* <p>This method creates an OutputStream with ZIP archive from the list of resources parameter.</p>
*
* @param zout OutputStrem on ZIP archive that will contain archives of resource files.
* @param method The compression method is a static int constant from ZipOutputSteeam with
* two theorical possible values :
* <ul>
* <li>{@link ZipOutputStream#DEFLATED} to compress archive.</li>
* <li>{@link ZipOutputStream#STORED} to let the archive uncompressed (unsupported).</li>
* </ul>
* @param level The compression level is an integer between 0 (not compressed) to 9 (best compression).
* @param resources Iterator other Path to compress
* @throws IOException
*/
private static void zipCore(final ZipOutputStream zout, final int method, final int level, final String entryPath, final Iterator<Path> resources)
throws IOException {
final CRC32 crc = new CRC32();
final ByteBuffer byteBuffer = ByteBuffer.allocate(BUFFER_SIZE);
boolean stored = false;
if (ZipOutputStream.STORED == method) {
stored = true;
} else if (ZipOutputStream.DEFLATED != method) {
throw new IllegalArgumentException("This compression method is not supported.");
}
if (Double.isNaN(level) || Double.isInfinite(level) || level > 9 || level < 0) {
throw new IllegalArgumentException("Illegal compression level.");
}
while (resources.hasNext()) {
final Path resource = resources.next();
final String fileName = resource.getFileName().toString();
final ZipEntry entry = new ZipEntry(entryPath + fileName);
if (stored) {
final long size = Files.size(resource);
entry.setCompressedSize(size);
entry.setSize(size);
entry.setCrc(crc.getValue());
}
if (Files.isDirectory(resource)) {
try( DirectoryStream<Path> children = Files.newDirectoryStream(resource)) {
final String nextEntryPath = entryPath + fileName + '/';
zipCore(zout, method, level, nextEntryPath, children.iterator());
continue;
}
}
zout.putNextEntry(entry);
if (stored) {
try (SeekableByteChannel sbc = Files.newByteChannel(resource, StandardOpenOption.READ)) {
crc.reset();
int bytesRead;
while ((bytesRead = sbc.read(byteBuffer)) != -1) {
crc.update(byteBuffer.array(), 0, bytesRead);
}
}
}
try (InputStream is = Files.newInputStream(resource)){
IOUtilities.copy(is, zout);
} finally {
zout.closeEntry();
}
}
}
/**
* <p>This method extracts a ZIP archive into the directory which contents it.</p>
*
* @param zip The archive parameter as File, URL, URI, InputStream or String path.
* This argument cannot be null.
* @param checksum Checksum object (instance of Alder32 or CRC32).
* @throws IOException
*/
public static List<Path> unzip(final Path zip, final Checksum checksum) throws IOException {
return unzip(zip, zip.getParent(), checksum);
}
/**
* <p>This method extract a ZIP archive into the specified location.</p>
*
* <p>ZIP resource parameter
*
* @param zip The archive parameter can be instance of InputStream, File,
* URL, URI or a String path. This argument cannot be null.
* @param resource The resource where archive content will be extracted.
* This resource location can be specified as instance of File or a String path.
* This argument cannot be null.
* @param checksum Checksum object (instance of Alder32 or CRC32).
* @throws IOException
*/
public static List<Path> unzip(final Path zip, final Path resource, final Checksum checksum) throws IOException {
try (InputStream inStream = Files.newInputStream(zip)) {
if (checksum != null) {
try (CheckedInputStream cis = new CheckedInputStream(inStream, checksum)) {
try (BufferedInputStream buf = new BufferedInputStream(cis)) {
return unzipCore(buf, resource);
}
}
} else {
try (BufferedInputStream buf = new BufferedInputStream(inStream)) {
return unzipCore(buf, resource);
}
}
}
}
/**
* <p>This method extract a ZIP archive from an InputStream, into directory
* whose path is indicated by resource object.</p>
*
* @param zip InputStream on ZIP resource that contains resources to extract.
* @param resource The resource where files will be extracted.
* Must be instance of File or a String path. This argument cannot be null.
* @throws IOException
*/
private static List<Path> unzipCore(final InputStream zip, final Path resource) throws IOException {
final List<Path> unzipped = new ArrayList<>();
try (ZipInputStream zis = new ZipInputStream(zip)) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
final Path file = resource.resolve(entry.getName());
if (entry.isDirectory()) {
Files.createDirectories(file);
continue;
}
Files.createDirectories(file.getParent());
unzipped.add(file);
try (OutputStream fos = Files.newOutputStream(file)) {
IOUtilities.copy(zis, fos);
}
}
}
return unzipped;
}
}