/**
 * ZipSnap 2.1
 * Copyright 2007 Zach Scrivena
 * 2007-08-26
 * zachscrivena@gmail.com
 * http://zipsnap.sourceforge.net/
 *
 * ZipSnap is a simple command-line incremental backup tool for directories.
 *
 * TERMS AND CONDITIONS:
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package zipsnap;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;


/**
 * Simple class for performing common compression input/output operations.
 */
public class CompressionIO
{
    /**
    * Add a given list of files/directories to a specified ZIP volume.
    *
    * @param zipFile
    *     ZIP volume to be created
    * @param files
    *     The files/directories to be added to the ZIP volume
    * @param addedFiles
    *     The files/directories that were successfully added to the ZIP volume
    * @return
    *     Number of files/directories successfully added to the ZIP volume
    */
    public static int zipFiles(
            final File zipFile,
            final List<FileUnit> files,
            final List<FileUnit> addedFiles)
    {
        final String zipFileName = zipFile.getName();

        if (ZipSnap.simulateOnly)
        {
            return simulateProcessFiles(
                    "\n\nSimulating addition of " + files.size() + " files/directories to new ZIP volume \"" + zipFileName + "\":",
                    "",
                    "No. of files/directories added:",
                    true,
                    files,
                    addedFiles);
        }

        System.out.print("\n\nAdding " + files.size() + " files/directories to new ZIP volume \"" + zipFileName + "\":");

        final File parentDir = zipFile.getParentFile();

        /* create parent directories if necessary */
        if (!parentDir.exists())
            parentDir.mkdirs();

        /* check that the parent directory of the ZIP volume exists */
        if (!parentDir.isDirectory())
            ErrorWarningHandler.reportErrorAndExit("Unable to create ZIP volume \"" + zipFileName + "\":\nThe parent directory \"" +
                    parentDir.getPath() + "\" does not exist, and cannot be created.");

        /* check if the ZIP volume already exists */
        if (zipFile.exists())
            ErrorWarningHandler.reportErrorAndExit("Unable to create ZIP volume \"" + zipFileName + "\":\nA " +
                    (zipFile.isDirectory() ? "directory" : "file") + " of the same name already exists.");

        /* return value */
        int numAddedFiles = 0;

        /* sum of uncompressed file/directory sizes */
        long totalUncompressedSize = 0;

        if (files.isEmpty())
        {
            /* create an empty file */
            try
            {
                final FileOutputStream fos = new FileOutputStream(zipFile);
                fos.flush();
                fos.close();
            }
            catch (Exception e)
            {
                ErrorWarningHandler.reportErrorAndExit("Unable to create ZIP volume \"" + zipFileName + "\":\n" +
                        ErrorWarningHandler.getExceptionMessage(e));
            }
        }
        else
        {
            /* at least one file/directory to be added */
            ZipOutputStream zos = null;

            try
            {
                zos = new ZipOutputStream(new FileOutputStream(zipFile));
            }
            catch (Exception e)
            {
                ErrorWarningHandler.reportErrorAndExit("Unable to create ZIP volume \"" + zipFileName + "\":\n" +
                        ErrorWarningHandler.getExceptionMessage(e));
            }

            /* set compression level (0-9) */
            zos.setLevel(ZipSnap.compressionLevel);

            final byte byteBuffer[] = new byte[ZipSnap.bufferSize];
            int i = 0;

            AddNextFile:
            for (FileUnit u : files)
            {
                i++;

                /* populate ZipEntry properties */
                final ZipEntry ze = new ZipEntry(u.name);
                ze.setTime(u.time);

                /* decide on compression method */
                final boolean useDeflate =
                        (u.size < ZipSnap.minSizeForCompression) ?
                        false : true;

                if (useDeflate)
                {
                    /* DEFLATE the file */
                    /* (size,CRC) fields of ZipEntry are automatically set */
                    System.out.print("\n  [" + i + "] \"" + u.nativeName + "\"");

                    /* set properties of ZipEntry */
                    ze.setMethod(ZipEntry.DEFLATED);
                }
                else
                {
                    /* STORE the file */
                    /* (size,CRC) fields of ZipEntry must be explicitly set */
                    System.out.print("\n  [" + i + "] \"" + u.nativeName + "\"");

                    /* set properties of ZipEntry */
                    ze.setCrc(u.getCrc());
                    ze.setSize(u.size);
                    ze.setMethod(ZipEntry.STORED);
                }

                System.out.flush();

                try
                {
                    /* commit Zip Entry to the ZIP volume */
                    zos.putNextEntry(ze);

                    /* read the file and compress the data if it is a file */
                    if (!u.isDirectory)
                    {
                        final FileInputStream fis = new FileInputStream(u.file);

                        while (true)
                        {
                            final int byteCount = fis.read(byteBuffer, 0, ZipSnap.bufferSize);

                            if (byteCount == -1)
                                break; /* reached EOF */

                            zos.write(byteBuffer, 0, byteCount);
                        }

                        fis.close();
                    }

                    /* close the zip stream and prepare for next entry */
                    zos.closeEntry();
                }
                catch (Exception e)
                {
                    if (u.isDirectory)
                    {
                        ErrorWarningHandler.reportWarning("Unable to add directory \"" + u.nativeName +
                                "\" to ZIP volume \"" + zipFileName + "\":\n" +
                                ErrorWarningHandler.getExceptionMessage(e) + "\nThis directory will be ignored.");
                    }
                    else
                    {
                        ErrorWarningHandler.reportWarning("Unable to add file \"" + u.nativeName +
                                "\" to ZIP volume \"" + zipFileName + "\":\n" +
                                ErrorWarningHandler.getExceptionMessage(e) + "\nThis file will be ignored.");
                    }

                    continue AddNextFile;
                }

                if (useDeflate)
                {
                    u.setCrc(ze.getCrc());

                    System.out.print(" (" + (int) (100.0 * (ze.getSize() - ze.getCompressedSize()) /
                            ze.getSize()) + "%)");
                }

                /* file/directory successfully added to ZIP volume */
                addedFiles.add(u);
                totalUncompressedSize += u.size;
                numAddedFiles++;
            }

            /* close the ZIP stream */
            try
            {
                zos.flush();
                zos.close();
            }
            catch (Exception e)
            {
                ErrorWarningHandler.reportWarning("Unable to close ZIP volume \"" + zipFileName +
                        "\"\n(ZIP volume may not be written successfully):\n" +
                        ErrorWarningHandler.getExceptionMessage(e));
            }
        }

        /* print summary */
        System.out.print(
            "\n  -------------------------------" +
            "\n  No. of files/directories added: " +
            numAddedFiles + " out of " + files.size() +
            "\n  Uncompressed size             : " +
            StringManipulator.formattedLong(totalUncompressedSize) + " bytes" +
            "\n  Compressed size (ratio)       : " +
            StringManipulator.formattedLong(zipFile.length()) + " bytes (" +
            ((totalUncompressedSize > 0) ?
                (int) (100.0 * (totalUncompressedSize - zipFile.length()) /
                totalUncompressedSize) :
                "-") +
            "%)");
        System.out.flush();

        /* set timestamp of ZIP volume */
        final FileIO.FileIOResult result = FileIO.setFileTime(zipFile, ZipSnap.currentTime.getTime());

        if (!result.success)
            ErrorWarningHandler.reportWarning("Unable to set last-modified time of ZIP volume \"" + zipFileName + "\".");

        return numAddedFiles;
    }


    /**
    * Add a given list of files/directories to a specified JAR volume.
    *
    * @param jarFile
    *     JAR volume to be created
    * @param files
    *     The files/directories to be added to the JAR volume
    * @param addedFiles
    *     The files/directories that were successfully added to the JAR volume
    * @return
    *     Number of files/directories successfully added to the JAR volume
    */
    public static int jarFiles(
            final File jarFile,
            final List<FileUnit> files,
            final List<FileUnit> addedFiles)
    {
        final String jarFileName = jarFile.getName();

        if (ZipSnap.simulateOnly)
        {
            return simulateProcessFiles(
                    "\n\nSimulating addition of " + files.size() + " files/directories to new JAR volume \"" + jarFileName + "\":",
                    "",
                    "No. of files/directories added:",
                    true,
                    files,
                    addedFiles);
        }

        System.out.print("\n\nAdding " + files.size() + " files/directories to new JAR volume \"" + jarFileName + "\":");

        final File parentDir = jarFile.getParentFile();

        /* create parent directories if necessary */
        if (!parentDir.exists())
            parentDir.mkdirs();

        /* check that the parent directory of the JAR volume exists */
        if (!parentDir.isDirectory())
            ErrorWarningHandler.reportErrorAndExit("Unable to create JAR volume \"" + jarFileName + "\":\nThe parent directory \"" +
                    parentDir.getPath() + "\" does not exist, and cannot be created.");

        /* check if the JAR volume already exists */
        if (jarFile.exists())
            ErrorWarningHandler.reportErrorAndExit("Unable to create JAR volume \"" + jarFileName + "\":\nA " +
                    (jarFile.isDirectory() ? "directory" : "file") + " of the same name already exists.");

        /* return value */
        int numAddedFiles = 0;

        /* sum of uncompressed file/directory sizes */
        long totalUncompressedSize = 0;

        if (files.isEmpty())
        {
            /* create an empty file and return */
            try
            {
                final FileOutputStream fos = new FileOutputStream(jarFile);
                fos.flush();
                fos.close();
            }
            catch (Exception e)
            {
                ErrorWarningHandler.reportErrorAndExit("Unable to create JAR volume \"" + jarFileName + "\":\n" +
                        ErrorWarningHandler.getExceptionMessage(e));
            }
        }
        else
        {
            /* at least one file/directory to be added */
            JarOutputStream jos = null;

            try
            {
                jos = new JarOutputStream(new FileOutputStream(jarFile));
            }
            catch (Exception e)
            {
                ErrorWarningHandler.reportErrorAndExit("Unable to create JAR volume \"" + jarFileName + "\":\n" +
                        ErrorWarningHandler.getExceptionMessage(e));
            }

            /* set compression level (0-9) */
            jos.setLevel(ZipSnap.compressionLevel);

            final byte byteBuffer[] = new byte[ZipSnap.bufferSize];
            int i = 0;

            AddNextFile:
            for (FileUnit u : files)
            {
                i++;

                /* populate JarEntry properties */
                final JarEntry je = new JarEntry(u.name);
                je.setTime(u.time);

                /* decide on compression method */
                final boolean useDeflate =
                        (u.size < ZipSnap.minSizeForCompression) ?
                        false : true;

                if (useDeflate)
                {
                    /* DEFLATE the file */
                    /* (size,CRC) fields of JarEntry are automatically set */
                    System.out.print("\n  [" + i + "] \"" + u.nativeName + "\"");

                    /* set properties of JarEntry */
                    je.setMethod(JarEntry.DEFLATED);
                }
                else
                {
                    /* STORE the file */
                    /* (size,CRC) fields of JarEntry must be explicitly set */
                    System.out.print("\n  [" + i + "] \"" + u.nativeName + "\"");

                    /* set properties of JarEntry */
                    je.setCrc(u.getCrc());
                    je.setSize(u.size);
                    je.setMethod(JarEntry.STORED);
                }

                System.out.flush();

                try
                {
                    /* commit Jar Entry to the JAR volume */
                    jos.putNextEntry(je);

                    /* read the file and compress the data if it is a file */
                    if (!u.isDirectory)
                    {
                        final FileInputStream fis = new FileInputStream(u.file);

                        while (true)
                        {
                            final int byteCount = fis.read(byteBuffer, 0, ZipSnap.bufferSize);

                            if (byteCount == -1)
                                break; /* reached EOF */

                            jos.write(byteBuffer, 0, byteCount);
                        }

                        fis.close();
                    }

                    /* close the JAR stream and prepare for next entry */
                    jos.closeEntry();
                }
                catch (Exception e)
                {
                    if (u.isDirectory)
                    {
                        ErrorWarningHandler.reportWarning("Unable to add directory \"" + u.nativeName +
                                "\" to JAR volume \"" + jarFileName + "\":\n" +
                                ErrorWarningHandler.getExceptionMessage(e) + "\nThis directory will be ignored.");
                    }
                    else
                    {
                        ErrorWarningHandler.reportWarning("Unable to add file \"" + u.nativeName +
                                "\" to JAR volume \"" + jarFileName + "\":\n" +
                                ErrorWarningHandler.getExceptionMessage(e) + "\nThis file will be ignored.");
                    }

                    continue AddNextFile;
                }

                if (useDeflate)
                {
                    u.setCrc(je.getCrc());

                    System.out.print(" (" + (int) (100.0 * (je.getSize() - je.getCompressedSize()) /
                            je.getSize()) + "%)");
                }

                /* file/directory successfully added to JAR volume */
                addedFiles.add(u);
                totalUncompressedSize += u.size;
                numAddedFiles++;
            }

            /* close the JAR stream */
            try
            {
                jos.flush();
                jos.close();
            }
            catch (Exception e)
            {
                ErrorWarningHandler.reportWarning("Unable to close JAR volume \"" + jarFileName +
                        "\"\n(JAR volume may not be written successfully):\n" +
                        ErrorWarningHandler.getExceptionMessage(e));
            }
        }

        /* print summary */
        System.out.print(
            "\n  -------------------------------" +
            "\n  No. of files/directories added: " +
            numAddedFiles + " out of " + files.size() +
            "\n  Uncompressed size             : " +
            StringManipulator.formattedLong(totalUncompressedSize) + " bytes" +
            "\n  Compressed size (ratio)       : " +
            StringManipulator.formattedLong(jarFile.length()) + " bytes (" +
            ((totalUncompressedSize > 0) ?
                (int) (100.0 * (totalUncompressedSize - jarFile.length()) /
                totalUncompressedSize) :
                "-") +
            "%)");
        System.out.flush();

        /* set timestamp of JAR volume */
        final FileIO.FileIOResult result = FileIO.setFileTime(jarFile, ZipSnap.currentTime.getTime());

        if (!result.success)
            ErrorWarningHandler.reportWarning("Unable to set last-modified time of JAR volume \"" + jarFileName + "\".");

        return numAddedFiles;
    }


    /**
    * Extract a given list of files/directories from a specified ZIP volume.
    *
    * @param zipFile
    *     ZIP volume from which files/directories are to be extracted
    * @param files
    *     The files/directories to be extracted from the ZIP volume
    * @param extractedFiles
    *     The files/directories that were successfully extracted from the ZIP volume
    * @return
    *     Number of files/directories successfully extracted from the ZIP volume
    */
    public static int unZipFiles(
            final File zipFile,
            final List<FileUnit> files,
            final List<FileUnit> extractedFiles)
    {
        final String zipFileName = (ZipSnap.searchPaths.size() == 1) ? zipFile.getName() : zipFile.getPath();

        if (ZipSnap.simulateOnly)
        {
            return simulateProcessFiles(
                    "\n\nSimulating extraction of " + files.size() + " files/directories from ZIP volume \"" + zipFileName + "\":",
                    "",
                    "No. of files/directories extracted:",
                    true,
                    files,
                    extractedFiles);
        }

        System.out.print("\n\nExtracting " + files.size() + " files/directories from ZIP volume \"" + zipFileName + "\":");

        /* return value */
        int numExtractedFiles = 0;

        if (!files.isEmpty())
        {
            /* at least one file to extract */

            /* check if the ZIP volume exists */
            if (!zipFile.exists())
                ErrorWarningHandler.reportWarning("ZIP volume \"" + zipFileName + "\" does not exist;\nthe " +
                        files.size() + " files/directories from this volume will not be extracted.");

            ZipFile zf = null;

            try
            {
                zf =  new ZipFile(zipFile);
            }
            catch (Exception e)
            {
                ErrorWarningHandler.reportWarning("Unable to open ZIP volume \"" + zipFileName + "\":\n" +
                        ErrorWarningHandler.getExceptionMessage(e) + "\nThe " + files.size() +
                        " files/directories from this volume will not be extracted.");
            }

            final byte byteBuffer[] = new byte[ZipSnap.bufferSize];
            int i = 0;

            ExtractNextFile:
            for (FileUnit u : files)
            {
                ++i;

                boolean extractFile = false;

                if (u.file.exists())
                {
                    /* a file/directory of the same name already exists */

                    if (u.isDirectory == u.file.isDirectory())
                    {
                        /* overwrite existing file/directory? */

                        if (ZipSnap.defaultActionOnOverwrite == 'Y')
                        {
                            System.out.print("\n  [" + i + "] Overwriting \"" + u.nativeName + "\"");

                            extractFile = true;
                        }
                        else if (ZipSnap.defaultActionOnOverwrite == 'N')
                        {
                            System.out.print("\n  [" + i + "] Skipping existing \"" + u.nativeName + "\"");
                        }
                        else if (ZipSnap.defaultActionOnOverwrite == '\0')
                        {
                            System.out.print("\n  [" + i + "] Overwrite \"" + u.nativeName + "\"?\n  ");

                            final char choice = UserIO.userCharPrompt(
                                    "(Y)es/(N)o/(A)lways/Neve(R): ",
                                    "YNAR");

                            if (choice == 'Y')
                            {
                                extractFile = true;
                            }
                            else if (choice == 'A')
                            {
                                ZipSnap.defaultActionOnOverwrite = 'Y';
                                extractFile = true;
                            }
                            else if (choice == 'R')
                            {
                                ZipSnap.defaultActionOnOverwrite = 'N';
                            }
                        }
                    }
                    else
                    {
                        System.out.print("\n  [" + i + "] \"" + u.nativeName + "\"");

                        ErrorWarningHandler.reportWarning("Unable to extract " + (u.isDirectory ? "directory" : "file") +
                                " \"" + u.nativeName + "\" from ZIP volume \"" + zipFileName + "\":\nA " +
                                (u.file.isDirectory() ? "directory" : "file") +
                                " of the same name already exists.");
                    }
                }
                else
                {
                    /* file/directory does not exist yet */
                    System.out.print("\n  [" + i + "] \"" + u.nativeName + "\"");
                    extractFile = true;
                }

                System.out.flush();

                if (extractFile)
                {
                    if (u.isDirectory)
                    {
                        /* extract a directory */
                        u.file.mkdirs();

                        if (!u.file.exists() || !u.file.isDirectory())
                        {
                            ErrorWarningHandler.reportWarning("Unable to extract directory \"" + u.nativeName +
                                    "\" from ZIP volume \"" + zipFileName + "\".");
                            continue ExtractNextFile;
                        }

                        /* check pathname of extracted directory */
                        String pathname = null;

                        try
                        {
                            pathname = u.file.getCanonicalPath();
                        }
                        catch (Exception e)
                        {
                            pathname = null;
                        }

                        if ((pathname != null) &&
                            !pathname.equals(u.file.getPath()))
                        {
                            /* rename extracted directory */
                            File s = new File(pathname);
                            File t = u.file;

                            while ((s != null) && (t != null) &&
                                    !s.equals(ZipSnap.currentDir) &&
                                    !t.equals(ZipSnap.currentDir) &&
                                    !s.getPath().equals(t.getPath()))
                            {
                                /* pathnames are different; proceed to rename */
                                s.renameTo(t);

                                /* check parent pathnames next */
                                s = s.getParentFile();
                                t = t.getParentFile();
                            }
                        }

                        /* set timestamp of extracted directory */
                        final FileIO.FileIOResult result = FileIO.setDirTime(u.file, u.time);

                        if (!result.success)
                            ErrorWarningHandler.reportWarning("Unable to set last-modified time of extracted directory \"" + u.nativeName + "\".");
                    }
                    else
                    {
                        /* extract a file */
                        final ZipEntry ze = zf.getEntry(u.name);

                        if (ze == null)
                        {
                            ErrorWarningHandler.reportWarning("Unable to extract file \"" +
                                    u.nativeName + "\" from ZIP volume \"" + zipFileName +
                                    "\":\nCannot find the corresponding entry in the ZIP volume.");
                            continue ExtractNextFile;
                        }

                        final File parentDir = u.file.getParentFile();

                        if ((parentDir != null) && !parentDir.exists())
                            parentDir.mkdirs();

                        if ((parentDir == null) || !parentDir.isDirectory())
                        {
                            ErrorWarningHandler.reportWarning("Unable to extract file \"" +
                                    u.nativeName + "\" from ZIP volume \"" + zipFileName +
                                    "\":\nThe parent directory of the file does not exist and cannot be created.");
                            continue ExtractNextFile;
                        }

                        /* write uncompressed data to file */
                        InputStream zis = null;
                        FileOutputStream fos = null;

                        try
                        {
                            zis = zf.getInputStream(ze);
                            fos = new FileOutputStream(u.file);

                            while (true)
                            {
                                final int byteCount = zis.read(byteBuffer, 0, ZipSnap.bufferSize);

                                if (byteCount == -1)
                                    break; /* reached EOF */

                                fos.write(byteBuffer, 0, byteCount);
                            }
                        }
                        catch (Exception e)
                        {
                            ErrorWarningHandler.reportWarning("Unable to extract file \"" +
                                    u.nativeName + "\" from ZIP volume \"" + zipFileName +
                                    "\":\n" + ErrorWarningHandler.getExceptionMessage(e));
                            continue ExtractNextFile;
                        }

                        try
                        {
                            fos.flush();
                            fos.close();
                        }
                        catch (Exception e)
                        {
                            ErrorWarningHandler.reportWarning("Unable to close extracted file \"" +
                                    u.nativeName + "\"\n(file may not be written successfully):\n" +
                                    ErrorWarningHandler.getExceptionMessage(e));
                        }

                        try
                        {
                            zis.close();
                        }
                        catch (Exception e)
                        {
                            ErrorWarningHandler.reportWarning("Unable to close ZIP volume entry \"" + u.nativeName +
                                    "\"\n(extracted file may not be written successfully):\n" +
                                    ErrorWarningHandler.getExceptionMessage(e));
                        }

                        /* check pathname of extracted file */
                        String pathname = null;

                        try
                        {
                            pathname = u.file.getCanonicalPath();
                        }
                        catch (Exception e)
                        {
                            pathname = null;
                        }

                        if ((pathname != null) &&
                            !pathname.equals(u.file.getPath()))
                        {
                            /* rename extracted file */
                            File s = new File(pathname);
                            File t = u.file;

                            while ((s != null) && (t != null) &&
                                    !s.equals(ZipSnap.currentDir) &&
                                    !t.equals(ZipSnap.currentDir) &&
                                    !s.getPath().equals(t.getPath()))
                            {
                                /* pathnames are different; proceed to rename */
                                s.renameTo(t);

                                /* check parent pathnames next */
                                s = s.getParentFile();
                                t = t.getParentFile();
                            }
                        }

                        /* set timestamp of extracted file */
                        final FileIO.FileIOResult result = FileIO.setFileTime(u.file, u.time);

                        if (!result.success)
                            ErrorWarningHandler.reportWarning("Unable to set last-modified time of extracted file \"" + u.nativeName + "\".");
                    }

                    /* file/directory successfully extracted from the ZIP volume */
                    extractedFiles.add(u);
                    numExtractedFiles++;
                }
            }

            /* close the ZIP volume */
            try
            {
                zf.close();
            }
            catch (Exception e)
            {
                ErrorWarningHandler.reportWarning("Unable to close ZIP volume \"" + zipFileName +
                        "\"\n(file may not be read successfully):\n" +
                        ErrorWarningHandler.getExceptionMessage(e));
            }
        }

        /* print summary */
        System.out.print(
            "\n  -----------------------------------" +
            "\n  No. of files/directories extracted: " +
            numExtractedFiles + " out of " + files.size());
        System.out.flush();

        return numExtractedFiles;
    }


    /**
    * Extract a given list of files/directories from a specified JAR volume.
    *
    * @param jarFile
    *     JAR volume from which files/directories are to be extracted
    * @param files
    *     The files/directories to be extracted from the JAR volume
    * @param extractedFiles
    *     The files/directories that were successfully extracted from the JAR volume
    * @return
    *     Number of files/directories successfully extracted from the JAR volume
    */
    public static int unJarFiles(
            final File jarFile,
            final List<FileUnit> files,
            final List<FileUnit> extractedFiles)
    {
        final String jarFileName = (ZipSnap.searchPaths.size() == 1) ? jarFile.getName() : jarFile.getPath();

        if (ZipSnap.simulateOnly)
        {
            return simulateProcessFiles(
                    "\n\nSimulating extraction of " + files.size() + " files/directories from JAR volume \"" + jarFileName + "\":",
                    "",
                    "No. of files/directories extracted:",
                    true,
                    files,
                    extractedFiles);
        }

        System.out.print("\n\nExtracting " + files.size() + " files/directories from JAR volume \"" + jarFileName + "\":");

        /* return value */
        int numExtractedFiles = 0;

        if (!files.isEmpty())
        {
            /* at least one file to extract */

            /* check if the JAR volume exists */
            if (!jarFile.exists())
                ErrorWarningHandler.reportWarning("JAR volume \"" + jarFileName + "\" does not exist;\nthe " +
                        files.size() + " files/directories from this volume will not be extracted.");

            JarFile jf = null;

            try
            {
                jf =  new JarFile(jarFile);
            }
            catch (Exception e)
            {
                ErrorWarningHandler.reportWarning("Unable to open JAR volume \"" + jarFileName + "\":\n" +
                        ErrorWarningHandler.getExceptionMessage(e) + "\nThe " + files.size() +
                        " files/directories from this volume will not be extracted.");
            }

            final byte byteBuffer[] = new byte[ZipSnap.bufferSize];
            int i = 0;

            ExtractNextFile:
            for (FileUnit u : files)
            {
                ++i;

                boolean extractFile = false;

                if (u.file.exists())
                {
                    /* a file/directory of the same name already exists */

                    if (u.isDirectory == u.file.isDirectory())
                    {
                        /* overwrite existing file/directory? */

                        if (ZipSnap.defaultActionOnOverwrite == 'Y')
                        {
                            System.out.print("\n  [" + i + "] Overwriting \"" + u.nativeName + "\"");

                            extractFile = true;
                        }
                        else if (ZipSnap.defaultActionOnOverwrite == 'N')
                        {
                            System.out.print("\n  [" + i + "] Skipping existing \"" + u.nativeName + "\"");
                        }
                        else if (ZipSnap.defaultActionOnOverwrite == '\0')
                        {
                            System.out.print("\n  [" + i + "] Overwrite \"" + u.nativeName + "\"?\n  ");

                            final char choice = UserIO.userCharPrompt(
                                    "(Y)es/(N)o/(A)lways/Neve(R): ",
                                    "YNAR");

                            if (choice == 'Y')
                            {
                                extractFile = true;
                            }
                            else if (choice == 'A')
                            {
                                ZipSnap.defaultActionOnOverwrite = 'Y';
                                extractFile = true;
                            }
                            else if (choice == 'R')
                            {
                                ZipSnap.defaultActionOnOverwrite = 'N';
                            }
                        }
                    }
                    else
                    {
                        System.out.print("\n  [" + i + "] \"" +
                                u.nativeName + "\"");

                        ErrorWarningHandler.reportWarning("Unable to extract " + (u.isDirectory ? "directory" : "file") +
                                " \"" + u.nativeName + "\" from JAR volume \"" + jarFileName + "\":\nA " +
                                (u.file.isDirectory() ? "directory" : "file") +
                                " of the same name already exists.");
                    }
                }
                else
                {
                    /* file/directory does not exist yet */
                    System.out.print("\n  [" + i + "] \"" + u.nativeName + "\"");
                    extractFile = true;
                }

                System.out.flush();

                if (extractFile)
                {
                    if (u.isDirectory)
                    {
                        /* extract a directory */
                        u.file.mkdirs();

                        if (!u.file.exists() || !u.file.isDirectory())
                        {
                            ErrorWarningHandler.reportWarning("Unable to extract directory \"" + u.nativeName +
                                    "\" from JAR volume \"" + jarFileName + "\".");
                            continue ExtractNextFile;
                        }

                        /* check pathname of extracted directory */
                        String pathname = null;

                        try
                        {
                            pathname = u.file.getCanonicalPath();
                        }
                        catch (Exception e)
                        {
                            pathname = null;
                        }

                        if ((pathname != null) &&
                            !pathname.equals(u.file.getPath()))
                        {
                            /* rename extracted directory */
                            File s = new File(pathname);
                            File t = u.file;

                            while ((s != null) && (t != null) &&
                                    !s.equals(ZipSnap.currentDir) &&
                                    !t.equals(ZipSnap.currentDir) &&
                                    !s.getPath().equals(t.getPath()))
                            {
                                /* pathnames are different; proceed to rename */
                                s.renameTo(t);

                                /* check parent pathnames next */
                                s = s.getParentFile();
                                t = t.getParentFile();
                            }
                        }

                        /* set timestamp of extracted directory */
                        final FileIO.FileIOResult result = FileIO.setDirTime(u.file, u.time);

                        if (!result.success)
                            ErrorWarningHandler.reportWarning("Unable to set last-modified time of extracted directory \"" + u.nativeName + "\".");
                    }
                    else
                    {
                        /* extract a file */
                        final ZipEntry ze = jf.getEntry(u.name);

                        if (ze == null)
                        {
                            ErrorWarningHandler.reportWarning("Unable to extract file \"" +
                                    u.nativeName + "\" from JAR volume \"" + jarFileName +
                                    "\":\nCannot find the corresponding entry in the JAR volume.");
                            continue ExtractNextFile;
                        }

                        final File parentDir = u.file.getParentFile();

                        if ((parentDir != null) && !parentDir.exists())
                            parentDir.mkdirs();

                        if ((parentDir == null) || !parentDir.isDirectory())
                        {
                            ErrorWarningHandler.reportWarning("Unable to extract file \"" +
                                    u.nativeName + "\" from JAR volume \"" + jarFileName +
                                    "\":\nThe parent directory of the file does not exist and cannot be created.");
                            continue ExtractNextFile;
                        }

                        /* write uncompressed data to file */
                        InputStream jis = null;
                        FileOutputStream fos = null;

                        try
                        {
                            jis = jf.getInputStream(ze);
                            fos = new FileOutputStream(u.file);

                            while (true)
                            {
                                final int byteCount = jis.read(byteBuffer, 0, ZipSnap.bufferSize);

                                if (byteCount == -1)
                                    break; /* reached EOF */

                                fos.write(byteBuffer, 0, byteCount);
                            }
                        }
                        catch (Exception e)
                        {
                            ErrorWarningHandler.reportWarning("Unable to extract file \"" +
                                    u.nativeName + "\" from JAR volume \"" + jarFileName +
                                    "\":\n" + ErrorWarningHandler.getExceptionMessage(e));
                            continue ExtractNextFile;
                        }

                        try
                        {
                            fos.flush();
                            fos.close();
                        }
                        catch (Exception e)
                        {
                            ErrorWarningHandler.reportWarning("Unable to close extracted file \"" +
                                    u.nativeName + "\"\n(file may not be written successfully):\n" +
                                    ErrorWarningHandler.getExceptionMessage(e));
                        }

                        try
                        {
                            jis.close();
                        }
                        catch (Exception e)
                        {
                            ErrorWarningHandler.reportWarning("Unable to close JAR volume entry \"" + u.nativeName +
                                    "\"\n(extracted file may not be written successfully):\n" +
                                    ErrorWarningHandler.getExceptionMessage(e));
                        }

                        /* check pathname of extracted file */
                        String pathname = null;

                        try
                        {
                            pathname = u.file.getCanonicalPath();
                        }
                        catch (Exception e)
                        {
                            pathname = null;
                        }

                        if ((pathname != null) &&
                            !pathname.equals(u.file.getPath()))
                        {
                            /* rename extracted file */
                            File s = new File(pathname);
                            File t = u.file;

                            while ((s != null) && (t != null) &&
                                    !s.equals(ZipSnap.currentDir) &&
                                    !t.equals(ZipSnap.currentDir) &&
                                    !s.getPath().equals(t.getPath()))
                            {
                                /* pathnames are different; proceed to rename */
                                s.renameTo(t);

                                /* check parent pathnames next */
                                s = s.getParentFile();
                                t = t.getParentFile();
                            }
                        }

                        /* set timestamp of extracted file */
                        final FileIO.FileIOResult result = FileIO.setFileTime(u.file, u.time);

                        if (!result.success)
                            ErrorWarningHandler.reportWarning("Unable to set last-modified time of extracted file \"" + u.nativeName + "\".");
                    }

                    /* file/directory successfully extracted from the JAR volume */
                    extractedFiles.add(u);
                    numExtractedFiles++;
                }
            }

            /* close the JAR volume */
            try
            {
                jf.close();
            }
            catch (Exception e)
            {
                ErrorWarningHandler.reportWarning("Unable to close JAR volume \"" + jarFileName +
                        "\"\n(file may not be read successfully):\n" +
                        ErrorWarningHandler.getExceptionMessage(e));
            }
        }

        /* print summary */
        System.out.print(
            "\n  -----------------------------------" +
            "\n  No. of files/directories extracted: " +
            numExtractedFiles + " out of " + files.size());
        System.out.flush();

        return numExtractedFiles;
    }


    /**
    * Simulate processing of specified files/directories.
    *
    * @param headerString
    *     String to be displayed before processing files/directories
    * @param perFileString
    *     String to be displayed for each file/directory processed
    * @param footerString
    *     String to be displayed after processing files/directories
    * @param success
    *     If true, all files/directories will be successfully processed;
    *     otherwise, all files/directories will not be successfully processed
    * @param files
    *     The files/directories to be processed
    * @param processedFiles
    *     The files/directories successfully processed
    * @return
    *     Number of files/directories successfully processed
    */
    public static int simulateProcessFiles(
            final String headerString,
            final String perFileString,
            final String footerString,
            final boolean success,
            final List<FileUnit> files,
            final List<FileUnit> processedFiles)
    {
        System.out.print(headerString);

        /* return value */
        int numProcessedFiles = 0;

        int i = 0;

        for (FileUnit u : files)
        {
            i++;

            System.out.print("\n  [" + i + "] " + perFileString + "\"" + u.nativeName + "\"");
            System.out.flush();

            if (success)
            {
                processedFiles.add(u);
                numProcessedFiles++;
            }
        }

        /* print summary */
        System.out.print(
            "\n  " + StringManipulator.repeat("-", footerString.length()) +
            "\n  " + footerString + " " +
            numProcessedFiles + " out of " + files.size());
        System.out.flush();

        return numProcessedFiles;
    }
}