3
\$\begingroup\$

I wrote a utility class that is used to extract the contents of a ZIP file to a destination folder. This is a very small class, but it has several practical applications: it can not only be used to extract a ZIP file, it can also be used to extract JAR and EPUB files.

UnZipper.java

package reader.utils;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

/**
 * This is a utility class that provides methods for extraction of data from
 * ZIP (and GZIP) files whilst making use of the {@linkplain java.util.zip}
 * package. All the public methods in this class are static and have the same
 * name: {@code unzip()}. Sample usage is as below:
 * <pre>
 * ...
 * String src = "(FULL path of ZIP file)";
 * String dst = "(FULL path of destination folder)";
 * try {
 *     UnZipper.unzip(src, dst);
 * } catch(IOException e) {
 *     e.printStackTrace();
 * }
 * ...
 * </pre>
 * <p>
 * <b>NOTE:</b>
 * <i>If the specified output directory is present, it will replace the
 * existing one and create a new one.</i>
 * This class can also be used to extract JAR and EPUB files.
 * </p>
 *
 * @author Subhomoy Haldar
 * @version 1.0
 */
public final class UnZipper {

    /**
     * <i>Demonstration.</i>
     *
     * @param args The command-line arguments.
     */
    public static void main(String[] args) {
        if (args.length != 2) {
            System.out.println("Sample usage:\n" +
                               "java UnZipper (full source file path) " +
                               "(full destination folder path");
            return;
        }
        String src = args[0];
        String dst = args[1];
        try {
            UnZipper.unzip(src, dst, Charset.forName("UTF-8"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * The maximum number of bytes read per iteration from the ZipInputStream.
     */
    private static final int BUFFER_SIZE = 4096; // in bytes

    // Prevent instantiation
    private UnZipper() {}

    /**
     * This method is used to unzip a ZIP file specified by the <i>full</i>
     * path given in the first {@code String} to a directory, whose
     * <i>full</i> path is given in the second {@code String}.
     * <p>
     * The <b>default charset</b> of the system is utilized.
     * </p>
     *
     * @param source      The <i>full</i> path of the ZIP (or JAR or EPUB) file.
     * @param destination The <i>full</i> path of the destination folder.
     * @throws IOException If a read/write error occurs.
     * @see Charset#defaultCharset()
     */
    public static void unzip(String source, String destination)
            throws IOException {
        unzip(new File(source), new File(destination));
    }

    /**
     * This method is used to unzip a ZIP file specified by the <i>full</i>
     * path given in the first {@code String} to a directory, whose
     * <i>full</i> path is given in the second {@code String}, whilst making
     * use of the {@code Charset} specified.
     *
     * @param source      The <i>full</i> path of the ZIP (or JAR or EPUB) file.
     * @param destination The <i>full</i> path of the destination folder.
     * @param charset     The {@code Charset} to be used during extraction.
     * @throws IOException IOException If a read/write error occurs.
     * @see Charset
     * @see Charset#forName(String)
     */
    public static void unzip(String source, String destination, Charset charset)
            throws IOException {
        unzip(new File(source), new File(destination), charset);
    }

    /**
     * This method is used to extract the ZIP file represented by the first
     * argument. The second parameter is the File that represents the
     * destination directory.
     * <p>
     * The <b>default charset</b> of the system is utilized.
     * </p>
     *
     * @param source      The {@code File} representing the ZIP file.
     * @param destination The {@code File} representing the destination folder.
     * @throws IOException If a read/write error occurs.
     */
    public static void unzip(File source, File destination)
            throws IOException {
        unzip(source, destination, Charset.defaultCharset());
    }

    /**
     * This method is used to extract the ZIP file represented by the first
     * argument. The second parameter is the File that represents the
     * destination directory. The given charset is used during extraction.
     *
     * @param source      The {@code File} representing the ZIP file.
     * @param destination The {@code File} representing the destination folder.
     * @param charset     The {@code Charset} to be used during extraction.
     * @throws IOException If a read/write error occurs.
     */
    public static void unzip(File source, File destination, Charset charset)
            throws IOException {
        try (ZipInputStream zipIn = new ZipInputStream
                (new FileInputStream(source), charset)) {
            // Make sure destination exists
            if (destination.exists()) {
                destination.delete();
            }
            destination.mkdir();
            ZipEntry entry = zipIn.getNextEntry();
            while (entry != null) {
                String filePath = destination + File.separator + entry.getName();
                File file = new File(filePath);
                if (entry.isDirectory()) {
                    file.mkdir();
                } else {
                    File parent = file.getParentFile();
                    if (!parent.exists()) {
                        parent.mkdir();
                    }
                    extractFile(zipIn, file);
                }
                zipIn.closeEntry();
                entry = zipIn.getNextEntry();
            }
        }
    }

    /**
     * This method extracts a file in a ZIP archive to the given destination
     * file.
     *
     * @param zipIn The ZipInputStream (source).
     * @param file  The File (destination).
     * @throws IOException If read/write error occurs.
     */
    private static void extractFile(ZipInputStream zipIn, File file)
            throws IOException {
        try (BufferedOutputStream outputStream = new BufferedOutputStream
                (new FileOutputStream(file))) {
            byte[] buffer = new byte[BUFFER_SIZE];
            int location;
            while ((location = zipIn.read(buffer)) != -1) {
                outputStream.write(buffer, 0, location);
            }
        }
    }
}

The problem is (I think) that the main unzip(File, File, Charset) is a little messy. Can the code here be improved (especially the mkdir() lines)? Also, is there any other way to do this better?

\$\endgroup\$
  • 1
    \$\begingroup\$ Unless you specifically need your class to work with Java 6, you should use Path instead of File. It will make your file operations more robust; for instance, you aren't checking the return value of File.mkdir, but when using Files.createDirectory, that won't be an issue. \$\endgroup\$ – VGR May 22 '15 at 16:16
5
\$\begingroup\$

Don't try to delete the destination

Before unzipping, this snippet seems to try to delete the destination directory if exists:

        if (destination.exists()) {
            destination.delete();
        }
        destination.mkdir();

But I don't think this works as you would expect. It will only delete the directory if it's not empty. But then it doesn't make sense to delete and then recreate.

Note that unzipping tools normally don't delete the destination by default. It would be dangerous.

I suggest to change to this:

        if (!destination.exists()) {
            destination.mkdir();
        }

Don't form paths from strings if you don't have to

Instead of this:

String filePath = destination + File.separator + entry.getName();
File file = new File(filePath);

It's simpler to use the 2-param constructor of File:

File file = new File(destination, entry.getName());

Bugs

Both branches of this if condition will fail in some cases:

    if (entry.isDirectory()) {
        file.mkdir();  // type 1
    } else {
        File parent = file.getParentFile();
        if (!parent.exists()) {
            parent.mkdir();  // type 2
        }
        extractFile(zipIn, file);
    }

Type 1: fails for a ZIP file whose first entry is a nested directory (unless the parent directories were previously created). For example if you create a ZIP file like this:

zip -r /tmp/test.zip some/nested/path/

Type 2: fails for a ZIP file whose first entry is a file in a nested directory (unless the parent directories were previously created).

For example if you create a ZIP file like this:

zip -r /tmp/test.zip some/nested/path/file.txt

You can fix both of these by changing the file.mkdir() to file.mkdirs().

Suggested implementation

With the suggestion in the last point, there's no more need to create the destination directory in advance, it will be created as needed. Which has the other advantage that if the zip is empty, a destination directory won't be unnecessarily created.

public static void unzip(File source, File destination, Charset charset) throws IOException {
    try (ZipInputStream zipIn = new ZipInputStream(new FileInputStream(source), charset)) {
        ZipEntry entry = zipIn.getNextEntry();
        while (entry != null) {
            File file = new File(destination, entry.getName());
            if (entry.isDirectory()) {
                file.mkdirs();
            } else {
                File parent = file.getParentFile();
                if (!parent.exists()) {
                    parent.mkdirs();
                }
                extractFile(zipIn, file);
            }
            zipIn.closeEntry();
            entry = zipIn.getNextEntry();
        }
    }
}
| improve this answer | |
\$\endgroup\$
  • 1
    \$\begingroup\$ What The Friedman! I knew there was something wrong with the mkdir() code. But I didn't know the exact details till you gave me the corner cases. Thanks a million! Your advice is really valuable, especially about the destination deletion case. :-) \$\endgroup\$ – Hungry Blue Dev May 17 '15 at 15:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.