/*
 * RHQ Management Platform
 * Copyright (C) 2005-2008 Red Hat, Inc.
 * All rights reserved.
 *
 * 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 version 2 of the License.
 *
 * 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, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */
package org.rhq.core.util;

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.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

import org.rhq.core.util.stream.StreamUtil;

/**
 * A set of utility methods for working with zip files.
 * 
 * @author Ian Springer
 */
public abstract class ZipUtil {

    /**
     * Zips up the given file or directory and stores the zip file at <code>zipFile</code>.
     * Note that zipping up a directory and if the output zip file is to be located in or under
     * the directory being zipped, an exception will be thrown - you must output the zip file
     * in another location outside of the directory being zipped.
     *  
     * @param fileOrDirToZip what to zip up
     * @param zipFile where to store the zip file
     * @throws IOException
     */
    public static void zipFileOrDirectory(File fileOrDirToZip, File zipFile) throws IOException {
        if (fileOrDirToZip.isDirectory()) {
            if (zipFile.getParentFile().getAbsolutePath().startsWith(fileOrDirToZip.getAbsolutePath())) {
                // if we allowed this, we could go in an infinite loop zipping up the every growing zip file
                throw new IOException("Cannot write the zip file [" + zipFile.getAbsolutePath()
                    + "] in or under the same directory being zipped [" + fileOrDirToZip.getAbsolutePath() + "]");
            }
        }

        ZipOutputStream zos = null;
        try {
            zos = new ZipOutputStream(new FileOutputStream(zipFile));
            zipFileOrDirectory(fileOrDirToZip, zos);
        } finally {
            if (zos != null) {
                zos.close();
            }
        }
    }

    public static void zipFileOrDirectory(File fileOrDirToZip, ZipOutputStream zos) throws IOException {
        if (fileOrDirToZip.isDirectory()) {
            File[] files = fileOrDirToZip.listFiles();
            for (File f : files) {
                zipFileOrDirectory(f, zos); // recurse
            }
        } else {
            FileInputStream fis = null;
            try {
                fis = new FileInputStream(fileOrDirToZip);
                ZipEntry zipEntry = new ZipEntry(fileOrDirToZip.getPath());
                zos.putNextEntry(zipEntry);
                StreamUtil.copy(fis, zos, false);
            } finally {
                if (fis != null) {
                    fis.close();
                }
            }
        }
    }

    /**
     * Unzips the content of the given zip file to the specified directory.
     *
     * @param  zipFile the zip file to unzip
     * @param  destDir root directory where the zip files will be extracted
     *
     * @throws IOException if any errors occur during the reading or writing
     */
    public static void unzipFile(File zipFile, File destDir) throws IOException {
        destDir.mkdirs();
        InputStream is = new BufferedInputStream(new FileInputStream(zipFile));
        ZipUtil.unzipFile(is, destDir);
    }

    /**
     * Writes the zip content out to the specified directory. The input stream should contain a valid
     * ZIP archive that will be extracted. NOTE: zipContent will be closed by this method.
     *
     * @param  zipContent   stream containing the content to write; should be formatted as a ZIP file
     * @param  outputDir root directory where the zip files will be extracted
     *
     * @throws IOException if any errors occur during the reading or writing
     */
    public static void unzipFile(InputStream zipContent, File outputDir) throws IOException {
        try {
            // First step is to create the output directory into which to unzip the content stream
            if (outputDir.exists() && !outputDir.isDirectory()) {
                throw new RuntimeException("Output directory already exists, but is a file, not a directory. File: "
                    + outputDir.getAbsolutePath());
            }

            if (!outputDir.exists()) {
                boolean directoryMade = outputDir.mkdirs();
                if (!directoryMade) {
                    throw new RuntimeException("Could not create output directory for unzipped artifact: "
                        + outputDir.getAbsolutePath());
                }
            }

            ZipInputStream zis = new ZipInputStream(zipContent);
            ZipEntry e;

            while ((e = zis.getNextEntry()) != null) {
                String entryFileName = e.getName();
                File entryFile = new File(outputDir, entryFileName);

                if (e.isDirectory()) {
                    entryFile.mkdirs();
                } else {

                    // JBNADM-3287 - For each file, make sure the directory it is to reside in is created.
                    String parentDirectory = entryFile.getParent();
                    File parentDirectoryFile = new File(parentDirectory);
                    parentDirectoryFile.mkdirs();

                    FileOutputStream fos = new FileOutputStream(entryFile);
                    try {
                        BufferedOutputStream outputStream = new BufferedOutputStream(fos);
                        try {
                            StreamUtil.copy(zis, outputStream, false);
                        } finally {
                            outputStream.close();
                        }
                    } finally {
                        fos.close();
                    }
                }
            }
        } finally {
            zipContent.close();
        }
    }

    /**
     * Walks the entries of a zip file, allowing a listener to "visit" each node and perform tasks on
     * the zip entry.
     *
     * @param zipFile the zip file to walk
     * @param visitor the object that will be notified for each entry in the zip file
     *
     * @throws Exception if any errors occur during the reading or visiting
     */
    public static void walkZipFile(File zipFile, ZipEntryVisitor visitor) throws Exception {
        FileInputStream fis = new FileInputStream(zipFile);
        try {
            InputStream zipContent = new BufferedInputStream(fis);
            try {
                ZipInputStream zis = new ZipInputStream(zipContent);

                ZipEntry e;
                while ((e = zis.getNextEntry()) != null) {
                    boolean keepGoing = visitor.visit(e, zis);
                    if (!keepGoing) {
                        break; // visitor told us to stop
                    }
                }
            } finally {
                zipContent.close();
            }
        } finally {
            fis.close();
        }
    }

    // prevent instantiation
    private ZipUtil() {
    }

    /**
     * Used by {@link ZipUtil#walkZipFile(File, ZipEntryVisitor)} to visit zip entries.
     */
    public static interface ZipEntryVisitor {
        /**
         * Visits a specific zip file entry. Implementations can read the entry content from the given stream but
         * must <b>not</b> close the stream - the caller of this method will handle the lifecycle of the stream.
         * 
         * @param entry the entry being visited
         * @param stream the stream containing the zip content
         * @return the visitor should return <code>true</code> if everything is OK and processing of the zip content
         *         should continue; returning <code>false</code> will tell the walker to abort further traversing
         *         of the zip content.
         * @throws Exception if the visitation failed for some reason - this will abort further walking of the zip content
         */
        boolean visit(ZipEntry entry, ZipInputStream stream) throws Exception;
    }
}
