
package uk.co.wingpath.util;

import java.io.*;
import java.util.*;

/**
* This class provides some static methods for working with files and
* directories.
*/
public final class FileUtils
{
    private FileUtils ()
    {
    }

    /**
    * Recursively deletes a directory and its contents. This method can also
    * be used to delete an ordinary file.
    * @param dir the directory to be deleted.
    * @return {@code true} if the deletion was successful, {@code false}
    * otherwise.
    */
    public static boolean deleteDir (File dir)
    {
        if (!dir.exists ())
            return true;
        boolean result = true;
        File [] files = dir.listFiles ();
        if (files != null)
        {
            for (File file : files)
            {
                if (!deleteDir (file))
                    result = false;
            }
        }
        if (!dir.delete ())
            result = false;
        return result;
    }

    /**
    * Adds a shutdown hook to recursively delete a directory.
    * @param dir the directory to be deleted.
    * @return the shutdown hook.
    */
    public static Thread deleteOnExit (final File dir)
    {
        Thread hook = new Thread (
            new Runnable ()
            {
                public void run ()
                {
                    deleteDir (dir);
                }
            },
            "Delete-Dir-" + dir.getName ());
        try
        {
            Runtime.getRuntime ().addShutdownHook (hook);
        }
        catch (IllegalStateException e)
        {
            // Already shutting down.
        }
        catch (SecurityException e)
        {
            // May happen if called from applet.
        }
        return hook;
    }

    /**
    * Prefix used for all generated names.
    * <p>Hopefully no one else will use this prefix, since we delete
    * directories whose names start with it!
    */
    private static final String TEMPDIR_PREFIX = "f3yoq9";

    /**
    * Generates a temporary name and creates a lock file using the name.
    * The lock file is created in the specified directory with the suffix
    * ".lck".
    * @param tmpdir temporary directory in which to create lock file.
    * @return generate name, without path or suffix.
    */
    private static String generateName (File tmpdir)
    {
        int randInt = new Random ().nextInt (9000);

        for (int attempt = 0 ; attempt < 50 ; attempt++)
        {
            randInt++;
            File file = new File (tmpdir, TEMPDIR_PREFIX + randInt + ".lck");
            try
            {
                if (file.createNewFile ())
                {
                    file.deleteOnExit ();
                    deleteOnExit (file);    // Belt and braces!
                    return TEMPDIR_PREFIX + randInt;
                }
            }
            catch (IOException e)
            {
            }
        }

        throw new RuntimeException ("Can't create temporary lock file");
    }

    /**
    * Removes any temporary directories that do not have associated lock
    * files.
    */
    private static void cleanupTempDirs ()
    {
        FileFilter filter = new FileFilter ()
            {
                public boolean accept (File file)
                {
                    return file.isDirectory () &&
                        file.getName ().startsWith (TEMPDIR_PREFIX);
                }
            };

        File tmpDir = new File (System.getProperty ("java.io.tmpdir"));

        for (File dir : tmpDir.listFiles (filter))
        {
            String name = dir.getName ();
            int suffix = name.lastIndexOf (".");
            String lockName = name.substring (0, suffix) + ".lck";
            File lockfile = new File (tmpDir, lockName);
            if (!lockfile.exists ())
                deleteDir (dir);
        }
    }

    /**
    * Creates a temporary directory.
    * <p>A shutdown hook is added to recursively delete the directory
    * when the program exits. Unfortunately this often doesn't work under
    * Windows, since files are often kept open (e.g. by class loaders) and
    * Windows won't delete open files.
    * <p>As a fallback, a temporary file is created with a name related to
    * the temporary directory.
    * This file is never opened, so we should be able to more reliably delete
    * it on exit.
    * Then, whenever we are creating a temporary directory, we scan for old
    * temporary directories and delete any that have no associated file.
    * @return the newly created directory (in canonical form).
    */
    public static File createTempDir ()
    {
        cleanupTempDirs ();
        File baseDir = new File (System.getProperty ("java.io.tmpdir"));
        String tmpname = generateName (baseDir);
        File dir = new File (baseDir, tmpname + ".tmp");
        if (!dir.mkdir ())
            throw new RuntimeException ("Can't create temporary directory");
        deleteOnExit (dir);
        return dir;
    }

    /**
    * Copies from an input stream (up to EOF) to an output stream.
    * @param in the input stream to copy from.
    * @param out the output stream to copy to.
    * @throws IOException if an I/O error occurs.
    */
    public static void copy (InputStream in, OutputStream out)
        throws IOException
    {
        byte [] buf = new byte [10000];
        int n;

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

    /**
    * Creates the parent directories of a file if they don't already exist.
    * @param file the file whose parent directories are to be created.
    */
    private static void createDirs (File file)
        throws IOException
    {
        File parent = file.getParentFile ();
        if (parent != null && !parent.isDirectory ())
        {
            createDirs (parent);
// System.out.println ("mkdir " + parent);
            if (!parent.mkdir ())
            {
// System.out.println ("Can't create " + parent);
                throw new IOException ("Can't create directory " + parent);
            }
        }
    }

    /**
    * Copies from an input stream (up to EOF) to a file.
    * The file (with its parent directories) is created if necessary,
    * or overwritten if it already exists.
    * @param in the input stream to copy from.
    * @param file the file to copy to.
    * @throws IOException if an I/O error occurs.
    */
    public static void copy (InputStream in, File file)
        throws IOException
    {
        createDirs (file);
        OutputStream out = null;
        try
        {
            out = new FileOutputStream (file);
            copy (in, out);
        }
        finally
        {
            if (out != null)
                out.close ();
        }
    }

    /** Copies from a file to another file.
    * The file (with its parent directories) is created if necessary,
    * or overwritten if it already exists.
    * @param inFile the file to copy from.
    * @param outFile the file to copy to.
    * @throws IOException if an I/O error occurs.
    */
    public static void copy (File inFile, File outFile)
        throws IOException
    {
        createDirs (outFile);
        InputStream in = null;
        try
        {
            in = new FileInputStream (inFile);
            copy (in, outFile);
        }
        finally
        {
            if (in != null)
                in.close ();
        }
    }

    /**
    * Copies from a "resource" to a file.
    * The file (with its parent directories) is created if necessary,
    * or overwritten if it already exists.
    * @param loader the class loader to be used to get the resource.
    * @param resource the name of the resource to copy from.
    * @param file the file to copy to.
    * @throws IOException if an I/O error occurs.
    */
    public static void copyResource (ClassLoader loader, String resource,
            File file)
        throws IOException
    {
// System.out.println ("copyResource " + resource + " " + file);
        createDirs (file);
        InputStream in = null;
        try
        {
            in = loader.getResourceAsStream (resource);
            if (in == null)
                throw new IOException ("Resource " + resource + " not found");
            copy (in, file);
        }
        finally
        {
            if (in != null)
                in.close ();
        }
    }

    /**
    * Copies from a "resource" to a file.
    * The system class loader is used to get the resource.
    * The file (with its parent directories) is created if necessary,
    * or overwritten if it already exists.
    * @param resource the name of the resource to copy from.
    * @param file the file to copy to.
    * @throws IOException if an I/O error occurs.
    */
    public static void copyResource (String resource, File file)
        throws IOException
    {
        copyResource (ClassLoader.getSystemClassLoader (), resource, file);
    }
}

