/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (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.apache.org/licenses/LICENSE-2.0
 *
 * 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 org.apache.openejb.util;

import org.apache.openejb.loader.FileUtils;
import org.apache.openejb.loader.Files;
import org.apache.openejb.loader.IO;
import org.apache.openejb.loader.SystemInstance;
import org.apache.openejb.loader.Zips;

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.nio.channels.FileChannel;
import java.util.Properties;

/**
 * @version $Rev$ $Date$
 */
public class JarExtractor {

    public static final Logger logger = Logger.getInstance(LogCategory.OPENEJB_STARTUP_CONFIG, "org.apache.openejb.util.resources");

    /**
     * Extract the Jar file into an unpacked directory structure, and
     * return the absolute pathname to the extracted directory.
     *
     * @param file     Jar file to unpack
     * @param pathname Context path name for web application
     * @throws IllegalArgumentException if this is not a "jar:" URL
     * @throws IOException              if an input/output error was encountered
     *                                  during expansion
     */
    public static File extract(final File file, final String pathname) throws IOException {

        final Properties properties = SystemInstance.get().getProperties();
        final String key = "tomee.unpack.dir";

        File unpackDir = file.getParentFile();

        if (properties.containsKey(key)) {
            final FileUtils base = SystemInstance.get().getBase();
            unpackDir = base.getDirectory(properties.getProperty(key), true);
        }

        File docBase = new File(unpackDir, pathname);

        docBase = extract(file, docBase);
        return docBase;
    }

    /**
     * Extract the jar file into the specifiec destination directory.  If the destination directory
     * already exists, the jar will not be unpacked.
     *
     * @param file           jar file to unpack
     * @param destinationDir the directory in which the jar will be unpacked; must not exist
     * @throws IOException if an input/output error was encountered during expansion
     */
    public static File extract(final File file, File destinationDir) throws IOException {
        if (destinationDir.exists()) {

            if (destinationDir.lastModified() > file.lastModified()) {
                // Ear file is already installed
                // Unpacked dir is newer than archive
                return destinationDir.getAbsoluteFile();
            }

            if (!deleteDir(destinationDir)) {
                Files.deleteOnExit(destinationDir);
                final File pf = destinationDir.getParentFile();
                final String name = destinationDir.getName() + System.currentTimeMillis();
                destinationDir = new File(pf, name);
                destinationDir.deleteOnExit();
                Files.deleteOnExit(destinationDir);
            }
        }

        logger.info("Extracting jar: " + file.getAbsolutePath());

        try {
            Files.mkdirs(destinationDir);
        } catch (final Files.FileRuntimeException e) {
            throw new IOException("Failed to create: " + destinationDir);
        }

        try {
            Zips.unzip(file, destinationDir);
        } catch (final IOException e) {
            // If something went wrong, delete extracted dir to keep things clean
            Files.delete(destinationDir);
            throw e;
        }

        // Return the absolute path to our new document base directory
        logger.info("Extracted path: " + destinationDir.getAbsolutePath());
        return destinationDir.getAbsoluteFile();
    }

    /**
     * Copy the specified file or directory to the destination.
     *
     * @param src  File object representing the source
     * @param dest File object representing the destination
     */
    public static boolean copyRecursively(final File src, final File dest) {

        boolean result = true;

        String[] files;

        if (src.isDirectory()) {
            files = src.list();
            result = dest.mkdir();
        } else {
            files = new String[1];
            files[0] = "";
        }

        if (files == null) {
            files = new String[0];
        }
        for (int i = 0; i < files.length && result; i++) {
            final File fileSrc = new File(src, files[i]);
            final File fileDest = new File(dest, files[i]);

            if (fileSrc.isDirectory()) {

                result = copyRecursively(fileSrc, fileDest);

            } else {

                FileChannel ic = null;
                FileChannel oc = null;
                try {
                    ic = new FileInputStream(fileSrc).getChannel();
                    oc = new FileOutputStream(fileDest).getChannel();
                    ic.transferTo(0, ic.size(), oc);
                } catch (final IOException e) {
                    logger.error("Copy failed: src: " + fileSrc + ", dest: " + fileDest, e);
                    result = false;
                } finally {
                    IO.close(ic);
                    IO.close(oc);
                }
            }
        }
        return result;

    }

    /**
     * Delete the specified directory, including all of its contents and
     * subdirectories recursively.
     *
     * @param dir File object representing the directory to be deleted
     */
    public static boolean delete(final File dir) {
        return deleteDir(dir);
    }

    /**
     * Delete the specified directory, including all of its contents and
     * subdirectories recursively.
     *
     * @param dir File object representing the directory to be deleted
     */
    public static boolean deleteDir(final File dir) {
        if (dir == null) {
            return true;
        }

        if (dir.isDirectory()) {
            final File[] files = dir.listFiles();
            if (files != null) {
                for (final File file : files) {
                    deleteDir(file);
                }
            }
        }

        return dir.delete();
    }

    /**
     * Extract the specified input stream into the specified directory, creating
     * a file named from the specified relative path.
     *
     * @param input   InputStream to be copied
     * @param docBase Document base directory into which we are extracting
     * @param name    Relative pathname of the file to be created
     * @return A handle to the extracted File
     * @throws IOException if an input/output error occurs
     */
    protected static File extract(final InputStream input, final File docBase, final String name)
        throws IOException {

        final File file = new File(docBase, name);
        BufferedOutputStream output = null;
        try {
            output = new BufferedOutputStream(new FileOutputStream(file));
            final byte[] buffer = new byte[2048];
            while (true) {
                final int n = input.read(buffer);
                if (n <= 0) {
                    break;
                }
                output.write(buffer, 0, n);
            }
        } finally {
            if (output != null) {
                try {
                    output.close();
                } catch (final IOException e) {
                    // Ignore
                }
            }
        }

        return file;
    }

}
