/*
 * $Id:$ 
 * IzPack - Copyright 2001-2008 Julien Ponge, All Rights Reserved.
 * 
 * http://izpack.org/ http://izpack.codehaus.org/
 * 
 * Copyright 2006 Klaus Bartz
 * 
 * Licensed 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 com.izforge.izpack.util;

import java.io.*;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * This class tries to remove a given list of files which are locked by this process. For this the
 * paths of the files are stored in a temporary file and a new process will be created. The class
 * files which are needed by the new process will be unpacked from the jar file under users temp dir
 * in a "sandbox". The new process receive the path of the temporary file and some other
 * information. After a wait intervall it reads the path file and removes all files which there are
 * listed. Next the created "sandbox" and the path file will be removed. This class uses the
 * characteristik of the system loader that jar files will be keeped open, simple class files will
 * be closed after loading a class. Therefore jar files are locked and cannot be deleted, class
 * files are not locked and deletable.<br>
 * The idea for this stuff is copied from Chadwick McHenry's SelfModifier in the uninstaller stuff
 * of IzPack.
 *
 * @author Klaus Bartz
 */
public class LibraryRemover
{

    /**
     * All class files which are needed for the second process. All have to be in this installers
     * jar file. No slash in front should be used; no dot else slashes should be used;
     * extension (.class) will be required.
     */
    private static final String[] SANDBOX_CONTENT = {"com/izforge/izpack/util/LibraryRemover.class"};

    /**
     * System property name of base for log and sandbox of secondary processes.
     */
    private static final String BASE_KEY = "lib.rem.base";

    /**
     * System property name of phase (1, 2, or 3) indicator.
     */
    private static final String PHASE_KEY = "self.mod.phase";

    /**
     * VM home Needed for the java command.
     */
    private static final String JAVA_HOME = System.getProperty("java.home");

    /**
     * Prefix of sandbox, path and log file.
     */
    private static final String PREFIX = "InstallRemover";

    /**
     * Phase of this process.
     */
    private int phase = 0;

    /**
     * Log for phase 2, because we can't capture the stdio from them.
     */
    private File logFile = null;

    /**
     * Directory which we extract too, invoke from, and finally delete.
     */
    private File sandbox = null;

    /**
     * The file which contains the paths of the files to delete.
     */
    private File specFile = null;

    /**
     * For logging time.
     */
    private SimpleDateFormat isoPoint = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");

    /**
     * Also for logging time.
     */
    private Date date = new Date();

    /**
     * Constructor for both phases. Depending on the phase different initializing will be performed.
     *
     * @param phase for which an object should be created.
     * @throws IOException
     */
    private LibraryRemover(int phase) throws IOException
    {
        this.phase = phase;
        if (phase == 1)
        {
            initJavaExec();
        }
        else
        {
            logFile = new File(System.getProperty(BASE_KEY) + ".log");
            specFile = new File(System.getProperty(BASE_KEY) + ".spec");
            sandbox = new File(System.getProperty(BASE_KEY) + ".d");
        }
    }

    /**
     * Entry point for phase 1. This class tries to remove all files given in the Vector.
     *
     * @param temporaryFileNames
     * @throws IOException
     */
    public static void invoke(List<String> temporaryFileNames) throws IOException
    {
        LibraryRemover self = new LibraryRemover(1);
        self.invoke1(temporaryFileNames);
    }

    /**
     * This call ensures that java can be exec'd in a separate process.
     *
     * @throws IOException       if an I/O error occurs, indicating java is unable to be exec'd
     * @throws SecurityException if a security manager exists and doesn't allow creation of a
     *                           subprocess
     */
    private void initJavaExec() throws IOException
    {
        try
        {
            Process p = Runtime.getRuntime().exec(javaCommand());

            new StreamProxy(p.getErrorStream(), "err").start();
            new StreamProxy(p.getInputStream(), "out").start();
            p.getOutputStream().close();

            // even if it returns an error code, it was at least found
            p.waitFor();
        }
        catch (InterruptedException ie)
        {
            throw new IOException("Unable to create a java subprocess");
        }
    }

    /**
     * Internal invoke method for phase 1.
     *
     * @param temporaryFileNames list of paths of the files which should be removed
     * @throws IOException
     */
    private void invoke1(List<String> temporaryFileNames) throws IOException
    {
        // Initialize sandbox and log file to be unique, but similarly named
        while (true)
        {
            logFile = File.createTempFile(PREFIX, ".log");
            String f = logFile.getCanonicalPath();
            specFile = new File(f.substring(0, f.length() - 4) + ".spec");
            sandbox = new File(f.substring(0, f.length() - 4) + ".d");

            // check if the similarly named directory is free
            if (!sandbox.exists())
            {
                break;
            }

            logFile.delete();
        }
        if (!sandbox.mkdir())
        {
            throw new RuntimeException("Failed to create temp dir: " + sandbox);
        }

        sandbox = sandbox.getCanonicalFile();
        logFile = logFile.getCanonicalFile();
        OutputStream out = null;
        InputStream in = null;
        byte[] buf = new byte[5120];
        int extracted = 0;
        // Write out the class files from the current installer jar into the sandbox.
        // This allows later to delete the classes because class files are deleteable
        // also the using process is running, jar files are not deletable in that
        // situation.,
        for (String aSANDBOX_CONTENT : SANDBOX_CONTENT)
        {
            in = getClass().getResourceAsStream("/" + aSANDBOX_CONTENT);

            File outFile = new File(sandbox, aSANDBOX_CONTENT);
            File parent = outFile.getParentFile();
            if (parent != null && !parent.exists())
            {
                parent.mkdirs();
            }

            out = new BufferedOutputStream(new FileOutputStream(outFile));

            int n;
            while ((n = in.read(buf, 0, buf.length)) > 0)
            {
                out.write(buf, 0, n);
            }

            out.close();
            extracted++;

        }
        // We write a file which contains the paths to remove.
        out = new BufferedOutputStream(new FileOutputStream(specFile));
        BufferedWriter specWriter = new BufferedWriter(new OutputStreamWriter(out));
        Iterator<String> iter = temporaryFileNames.iterator();
        while (iter.hasNext())
        {
            specWriter.write(iter.next());
            if (iter.hasNext())
            {
                specWriter.newLine();
            }
        }
        specWriter.flush();
        out.close();

        spawn(2);

        // finally, if all went well, the invoking process must exit
        log("library cleanup done");
        //
        // IZPACK-276:
        // Do never call System.exit during a cleanup otherwise the correct exit value is lost!
        // System.exit(0);
    }

    /**
     * Returns an ArrayList of the files to delete.
     *
     * @return The files list.
     * @throws Exception Description of the Exception
     */
    private ArrayList<File> getFilesList() throws Exception
    {
        // Initialisations
        TreeSet<File> files = new TreeSet<File>(Collections.reverseOrder());
        InputStream in = new FileInputStream(specFile);
        InputStreamReader inReader = new InputStreamReader(in);
        BufferedReader reader = new BufferedReader(inReader);

        // We read it
        String read = reader.readLine();
        while (read != null)
        {
            files.add(new File(read));
            read = reader.readLine();
        }
        in.close();
        // We return it
        return new ArrayList<File>(files);
    }

    /**
     * Invoke methode for phase 2.
     */
    private void invoke2()
    {

        try
        {
            // Give main program time to exit.
            try
            {
                Thread.sleep(1000);
            }
            catch (Exception x)
            {
            }

            ArrayList<File> files = getFilesList();
            int size = files.size();
            // We destroy the files

            log("deleteing temporary dlls/shls");
            for (int i = 0; i < size; i++)
            {
                File file = files.get(i);
                file.delete();
                if (file.exists())
                {
                    log("    deleting of " + file.getCanonicalPath() + " failed!!!");
                }
                else
                {
                    log("    " + file.getCanonicalPath());
                }

            }

            // clean up and go
            log("deleteing sandbox");
            deleteTree(sandbox);
            specFile.delete();
        }
        catch (Exception e)
        {
            log(e);
        }
    }

    /**
     * Copied from com.izforge.izpack.uninstaller.SelfModifier. Little addaption for this class.
     *
     * @param nextPhase phase of the spawn
     * @return created process object
     * @throws IOException
     */
    private Process spawn(int nextPhase) throws IOException
    {
        String base = logFile.getAbsolutePath();
        base = base.substring(0, base.length() - 4);

        // invoke from tmpdir, passing target method arguments as args, and
        // SelfModifier parameters as sustem properties
        String[] javaCmd = new String[]{javaCommand(), "-classpath", sandbox.getAbsolutePath(),
                "-D" + BASE_KEY + "=" + base, "-D" + PHASE_KEY + "=" + nextPhase,
                getClass().getName()};

        StringBuffer sb = new StringBuffer("Spawning phase ");
        sb.append(nextPhase).append(": ");
        for (String aJavaCmd : javaCmd)
        {
            sb.append("\n\t").append(aJavaCmd);
        }
        log(sb.toString());

        // Just invoke it and let it go, the exception will be caught above
        return Runtime.getRuntime().exec(javaCmd, null, null); // workDir);
    }

    /**
     * Recursively delete a file structure. Copied from com.izforge.izpack.uninstaller.SelfModifier.
     * Little addaption to this class.
     *
     * @return command for spawning
     */
    public static boolean deleteTree(File file)
    {
        if (file.isDirectory())
        {
            File[] files = file.listFiles();
            for (File file1 : files)
            {
                deleteTree(file1);
            }
        }
        return file.delete();
    }

    /**
     * Copied from com.izforge.izpack.uninstaller.SelfModifier.
     *
     * @return command command extended with extension of executable
     */
    private static String addExtension(String command)
    {
        // This is the most common extension case - exe for windows and OS/2,
        // nothing for *nix.
        return command + (OsVersion.IS_WINDOWS || OsVersion.IS_OS2 ? ".exe" : "");
    }

    /**
     * Copied from com.izforge.izpack.uninstaller.SelfModifier. Little addaption for this class.
     *
     * @return command for spawning
     */
    private static String javaCommand()
    {
        String executable = addExtension("java");
        String dir = new File(JAVA_HOME + "/bin").getAbsolutePath();
        File jExecutable = new File(dir, executable);

        // Unfortunately on Windows java.home doesn't always refer
        // to the correct location, so we need to fall back to
        // assuming java is somewhere on the PATH.
        if (!jExecutable.exists())
        {
            return executable;
        }
        return jExecutable.getAbsolutePath();
    }

    public static void main(String[] args)
    {
        // Phase 2 removes given path list, sandbox and spec file.
        // Phase 3 as used in SelfModifier will be not needed here because
        // this class do not use a GUI which can call exit like the
        // one in SelfModifier.

        try
        {
            // all it's attributes are retrieved from system properties
            LibraryRemover librianRemover = new LibraryRemover(2);
            librianRemover.invoke2();
        }
        catch (IOException ioe)
        {
            System.err.println("Error invoking a secondary phase");
            System.err.println("Note that this program is only intended as a secondary process");
            ioe.printStackTrace();
        }
    }

    /**
     * ********************************************************************************************
     * --------------------------------------------------------------------- Logging
     * --------------------------------------------------------------------- Copied from
     * com.izforge.izpack.uninstaller.SelfModifier.
     */

    PrintStream log = null;

    private PrintStream checkLog()
    {
        try
        {
            if (log == null)
            {
                log = new PrintStream(new FileOutputStream(logFile.toString(), true));
            }
        }
        catch (IOException x)
        {
            System.err.println("Phase " + phase + " log err: " + x.getMessage());
            x.printStackTrace();
        }
        date.setTime(System.currentTimeMillis());
        return log;
    }

    private void log(Throwable t)
    {
        if (checkLog() != null)
        {
            log.println(isoPoint.format(date) + " Phase " + phase + ": " + t.getMessage());
            t.printStackTrace(log);
        }
    }

    private void log(String msg)
    {
        if (checkLog() != null)
        {
            log.println(isoPoint.format(date) + " Phase " + phase + ": " + msg);
        }
    }

    public static class StreamProxy extends Thread
    {

        InputStream in;

        String name;

        OutputStream out;

        public StreamProxy(InputStream in, String name)
        {
            this(in, name, null);
        }

        public StreamProxy(InputStream in, String name, OutputStream out)
        {
            this.in = in;
            this.name = name;
            this.out = out;
        }

        public void run()
        {
            try
            {
                PrintWriter pw = null;
                if (out != null)
                {
                    pw = new PrintWriter(out);
                }

                BufferedReader br = new BufferedReader(new InputStreamReader(in));
                String line;
                while ((line = br.readLine()) != null)
                {
                    if (pw != null)
                    {
                        pw.println(line);
                    }
                    // System.out.println(name + ">" + line);
                }
                if (pw != null)
                {
                    pw.flush();
                }
            }
            catch (IOException ioe)
            {
                ioe.printStackTrace();
            }
        }
    }

}
