/*
 * Copyright 2006-2007 Queplix Corp.
 *
 * Licensed under the Queplix Public License, Version 1.1.1 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.queplix.com/solutions/commercial-open-source/queplix-public-license/
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 *
 */

package com.queplix.core.utils;

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.io.InputStream;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;

/**
 * Helper class for ZIP archives operations.
 *
 * @author [ONZ] Oleg N. Zhovtanyuk
 * @version $Revision: 1.1.1.1 $ $Date: 2005/09/12 15:31:17 $
 */

public final class ZipHelper {

    // ============================================================== Constants

    /**
     * Default Zip archive extension.
     */
    public static final String ZIP_EXT = ".zip";

    // I/O buffer size.
    private static final int BUFFER_SIZE = 1024;

    // ========================================================= Public methods

    /**
     * Zips the given file or directory.
     *
     * @param src the file or directory to zip
     * @param zip archive
     * @throws IOException on I/O errors
     */
    public static void zip(File src, File zip)
            throws IOException {
        ZipOutputStream sink = null;
        try {
            sink = new ZipOutputStream(new FileOutputStream(zip));
            doZip(sink, src, src);
        } finally {
            try {
                if(sink != null) {
                    sink.close();
                }
            } catch (IOException ex) {
            }
        }
    }

    /**
     * Zips the given files or directories.
     *
     * @param topDir top level directory to make relative paths from
     * @param src    the files or directories to zip
     * @param zip    archive
     * @throws IOException on I/O errors
     */
    public static void zip(File topDir, File[] src, File zip)
            throws IOException {

        ZipOutputStream sink = null;
        try {
            sink = new ZipOutputStream(new FileOutputStream(zip));
            for(int i = 0; i < src.length; i++) {
                File file = src[i];
                doZip(sink, topDir, file);
            }
        } finally {
            try {
                if(sink != null) {
                    sink.close();
                }
            } catch (IOException ex) {
            }
        }
    }

    /**
     * Unzips the given archive.
     *
     * @param zip     the archive to unzip
     * @param destDir destination directory
     * @throws IOException on I/O errors
     */
    public static void unzip(File zip, File destDir)
            throws IOException {

        Map dirs = doUnzip(zip, destDir);
        // TODO: implement recursive last modification date setup.
    }

    // ======================================================== Private methods

    /**
     * Adds a file entry to ZIP archive. For a directory recursively puts all
     * its subdirectories tree into archive.
     *
     * @param sink   ZIP output stream to use
     * @param topDir top directory used to build ZIP entries relative names
     * @param src    file to put into archive
     * @throws IOException
     */
    private static void doZip(ZipOutputStream sink, File topDir, File src)
            throws IOException {

        try {

            boolean isDirectory = src.isDirectory();
            boolean isFile = src.isFile();

            // Check for things like block devices or channels.
            if(!isDirectory && !isFile) {
                return;
            }

            String entryName = getEntryName(topDir, src);
            if(entryName == null) {

                // No entry, but...
                if(isDirectory) {
                    // ... top directory - add all content.
                    File[] files = src.listFiles();
                    if(files != null) {
                        for(int i = 0; i < files.length; i++) {
                            doZip(sink, topDir, files[i]);
                        }
                    }
                } else {
                    // ... file outside the top directory - link?
                    ZipEntry zipEntry = new ZipEntry(src.getName());
                    zipEntry.setTime(src.lastModified());
                    sink.putNextEntry(zipEntry);
                    file2entry(src, zipEntry, sink);
                    sink.closeEntry();
                }

            } else {

                // Regular directory / file entry...
                ZipEntry zipEntry = new ZipEntry(entryName);
                zipEntry.setTime(src.lastModified());
                sink.putNextEntry(zipEntry);
                if(isDirectory) {
                    // ... directory - add content.
                    File[] files = src.listFiles();
                    if(files != null) {
                        for(int i = 0; i < files.length; i++) {
                            doZip(sink, topDir, files[i]);
                        }
                    }
                } else {
                    // ... file - copy content via stream.
                    file2entry(src, zipEntry, sink);
                }
                sink.closeEntry();

            } // if (entryName == null)

        } catch (SecurityException ex) {
            throw new SecurityException(
                    "Security exception while zipping '" + src + "':" + ex
                            .getMessage());

        } catch (ZipException ex) {
            throw new IOException(
                    "Zip exception while zipping '" + src + "':" + ex
                            .getMessage());

        } catch (Throwable t) {
            throw new IOException(
                    "Unknown exception while zipping '" + src + "':" + t
                            .getMessage());
        }

    }

    /**
     * Unzips the given archive.
     *
     * @param zip     the archive to unzip
     * @param destDir destination directory
     * @return map of directory-timestamp pairs
     * @throws IOException on I/O errors
     */
    public static Map doUnzip(File zip, File destDir)
            throws IOException {

        ZipFile zipFile = null;
        Map dirs = new HashMap();

        try {

            // Unzip directories first.
            zipFile = new ZipFile(zip);
            for(Enumeration en = zipFile.entries(); en.hasMoreElements();) {
                ZipEntry zipEntry = (ZipEntry) en.nextElement();
                if(zipEntry.isDirectory()) {
                    File dir = new File(destDir, zipEntry.getName());
                    try {
                        if(!dir.exists()) {
                            dir.mkdirs();
                        }
                        dirs.put(dir, new Long(zipEntry.getTime()));
                    } catch (SecurityException ex) {
                        throw new SecurityException(
                                "Security exception while unzipping '" +
                                        dir + "':" + ex.getMessage());
                    }
                }
            }

            // Then files.
            zipFile = new ZipFile(zip);
            for(Enumeration en = zipFile.entries(); en.hasMoreElements();) {
                ZipEntry zipEntry = (ZipEntry) en.nextElement();
                if(!zipEntry.isDirectory()) {
                    File file = new File(destDir, zipEntry.getName());
                    try {
                        entry2file(zipFile, zipEntry, file);
                        file.setLastModified(zipEntry.getTime());
                    } catch (SecurityException ex) {
                        throw new SecurityException(
                                "Security exception while unzipping '" +
                                        file + "':" + ex.getMessage());
                    }
                }
            }

        } finally {
            if(zipFile != null) {
                zipFile.close();
            }
        }

        // Ok.
        return dirs;

    }

    /**
     * Gets ZIP archive entry name. The name is the name of a source file being
     * archived, relative to given top directory.
     *
     * @param topDir top directory
     * @param src    source file / directory
     * @return relative entry name
     */
    private static String getEntryName(File topDir, File src) {

        // Get paths.
        String topPath = topDir.getAbsolutePath();
        String path = src.getAbsolutePath();

        // Get entry name.
        String entryName = null;
        if(topPath.length() >= path.length()) {
            entryName = null;
        } else {
            entryName = path.substring(topPath.length() + 1).replace(
                    File.separatorChar, '/');
            if(src.isDirectory()) {
                entryName += "/";
            }
        }

        // Ok.
        return entryName;

    }

    // Copies file to ZIP entry via stream.
    private static void file2entry(File src, ZipEntry dest,
                                   ZipOutputStream sink)
            throws IOException {
        BufferedInputStream in = null;
        try {
            if(src.length() > 0) {
                in = new BufferedInputStream(new FileInputStream(src));
                byte[] buffer = new byte[BUFFER_SIZE];
                int len;
                while((len = in.read(buffer)) >= 0) {
                    sink.write(buffer, 0, len);
                }
            }
        } finally {
            try {
                if(in != null) {
                    in.close();
                }
            } catch (IOException ex) {
            }
        }
    }

    // Extracts ZIP entry to file via stream.
    private static void entry2file(ZipFile zip, ZipEntry src, File dest)
            throws IOException {
        InputStream in = null;
        BufferedOutputStream out = null;
        try {
            in = zip.getInputStream(src);
            out = new BufferedOutputStream(new FileOutputStream(dest));
            byte[] buffer = new byte[BUFFER_SIZE];
            int len;
            while((len = in.read(buffer)) >= 0) {
                out.write(buffer, 0, len);
            }
            out.flush();
        } finally {
            try {
                if(in != null) {
                    in.close();
                }
                if(out != null) {
                    out.close();
                }
            } catch (IOException ex) {
            }
        }
    }

}