package org.itscool.commons.util;

import java.io.*;
import java.util.logging.Level;
import java.util.logging.Logger;

/*
 * -----------------------------------------------------------------------------
 * 問題
 * -----------------------------------------------------------------------------
 * Javaのよく知られた2つのバグにより、JVMデザインとWin32オペレーティングシステム
 * を組み合わせて利用するJavaアプリケーションは、終了時にオープン状態になってい
 * るテンポラリファイルを削除することができません。
 * 
 * そのため、ユーザーのシステム上にテンポラリファイルを放置することになります。
 * 
 * -----------------------------------------------------------------------------
 * 解決法
 * -----------------------------------------------------------------------------
 * JVMが起動時に短時間のクリーンアップ処理を行って、アプリケーションを前回実行し
 * たときに残されたテンポラリファイルをすべて削除するようにします。
 * ただし、テンポラリファイルは作成時にロック指定する必要があるので、アプリケー
 * ションの特定インスタンスに関するテンポラリファイルをすべて1つのディレクトリに
 * 入れて、そのディレクトリをロックするようにします。
 * 
 * -----------------------------------------------------------------------------
 * ささいな原因が引き起こす大きな問題
 * -----------------------------------------------------------------------------
 * Java Foundation Classes（JFC）には、アプリケーションからテンポラリファイルを
 * 作成および破棄するための機能が用意されています。
 * 下記のコードのようにjava.io.Fileクラスを使用すると、アプリケーションから
 * テンポラリファイルを作成したり、削除対象としてマークしたりできます。
 *
 * File tempFile =    File.createTempFile("myApp", ".tmp");
 * tempFile.deleteOnExit();
 * 
 * deleteOnExit()メソッドは、Java Virtual Machine（JVM）の終了時にファイルを
 * 削除するために設計されたものです。
 * 
 * すべてが理想どおりに進めば、これで必要な処理がすべて行われるはずでした。
 * ところが私のアプリケーションでは、この行われるべき処理が実行されませんでした。
 * ファイルは削除対象として指定されてはいましたが、実際には削除されませんでした。
 * さらに複雑なことに、このアプリケーションをLinux上で実行した場合には、
 * テンポラリファイルが削除されました。
 * こうして私は問題の調査を始めました。
 *
 * 調査を進めた結果、ようやくこの問題の原因を発見しました。
 * 犯人は、Javaのよく知られた2つのバグでした。
 * 1つ目のバグは、バグ番号4171239「java.io.File.deleteOnExitは、
 * オープン状態のファイルに対しては動作しません（win32）」です。
 * つまり、JVMデザインとWin32オペレーティングシステムの組み合わせでは、オープン
 * 状態のファイルを終了時に削除できないのです。
 * では、終了前にすべてのファイルを閉じておけばいい、ということでしょうか？ 
 * そうはいきません。
 * これはバグ番号4950148「ClassLoaderの使用にあたっては、明示的な廃棄処理を設け
 * る必要があります」のバグに関係してきます。
 * これはつまり、URLClassLoader（JARファイルからクラスをロードする際に使用する
 * クラス）は、JVMのライフタイム中はJARファイルへのストリームをオープンにし続け
 * るということです。
 * 
 * ここで行き詰まりました。Win32 JVMはオープン状態のファイルを削除できず、
 * URLClassLoaderは常にファイルをオープン状態にしているのですから。
 * 先に進むには、何らかの迂回路を探す必要があります。
 *
 * 1つの単純な解決法としては、アプリケーションの作成するテンポラリファイルに
 * 共通の接頭辞を付けるようにしておき、特定のタイミングでこの接頭辞を持つすべて
 * のファイルを削除する、という方式が考えられます。
 * しかしこの方式には、同一アプリケーションの複数のインスタンスを同時実行すると
 * 競合状態が生じて、一方のインスタンスが使用しているテンポラリファイルを他方が
 * 削除してしまう危険性があるので、複数インスタンスを使用できなくなるという欠点
 * があります。
 * 
 * また、何か別のアプリケーションで同じ接頭辞を使用していると、この場合もテンポ
 * ラリファイルが想定外のシナリオで削除される可能性が生じます。
 * しかし、私が必要としているのは、堅牢性と再利用性に優れたソリューションであり、
 * それを使えばどのプロジェクトでも適切なクリーンアップ処理を実現できるという
 * ものです。
 *
 * -----------------------------------------------------------------------------
 * デザイン：より適切なファイルトラップの設計
 * -----------------------------------------------------------------------------
 * この問題を解決するには、テンポラリファイルマネージャを作成する必要がありまし
 * た。
 * ここでいうテンポラリファイルマネージャとは、テンポラリファイルを作成し、
 * 将来の何らかの時点でこれらを確実に廃棄するためのクラスです。
 * そもそもの原因がJVMのバグなので、アプリケーションの次回実行時までテンポラリ
 * ファイルが残留するとしても、それは容認することにしました。このように条件を
 * 少し緩めましたが、それでも、「アプリケーションの実行と実行の間にシステム上に
 * 残されるテンポラリファイルを1セットだけにする」ことを保証しなければなりません。
 *
 * たいていの場合、最善のソリューションとは最も簡単なものです。
 * JVMは、オープン状態のファイルでなければファイルを削除できるのですから、
 * 可能な限りこの機能を活用することにしました。
 * また私は、JVMの初回起動時にはオープン状態のテンポラリファイルは存在しない、
 * ということを知っていました。
 * 
 * したがって、前回実行時から放置されているファイルが存在しても、
 * これらを削除するクリーンアップ処理は即座に実行できるはずです。
 * さらに、テンポラリファイルは作成時にロック指定される必要があるので、
 * これと同じテンポラリファイル管理スキームを使う他のアプリケーションがあった
 * としても、使用中のファイルを削除しようとはしないはずです。
 * 
 * 私は、ロックファイルの数を減らすために、アプリケーションの特定インスタンス
 * に属するすべてのテンポラリファイルを1つのディレクトリに入れて、
 * このディレクトリごとロックするようにしました。
 *
 * ソリューションの設計はこれで完了したので、次にこれをコード化する必要が
 * あります。
 * 
 * -----------------------------------------------------------------------------
 * 実装：問題部分の修正
 * -----------------------------------------------------------------------------
 * シンプルさを確保するためには、JFCのFileクラスに似たAPIが必要でした。
 * そこで、テンポラリファイルの作成と既存ファイルの削除を扱わせるために、
 * TempFileManagerというマネージャクラスを作成しました。
 * 
 * このクラスには、テンポラリファイルのクリーンアップを容易にするための静的
 * メソッドが用意されています。
 * この静的メソッドは、JVMが次回このクラスをロードしたときにクリーンアップされる
 * 特別なディレクトリ内にテンポラリファイルを作成します。
 * 
 * リスト1：テンポラリファイルの生成とクリーンアップを正しく行うためのコード
 *  
 * 
 * @author Michiaki_Kano
 *
 * TODO この生成された型コメントのテンプレートを変更するには次へジャンプ:
 * ウィンドウ - 設定 - Java - コード・スタイル - コード・テンプレート
 */
/**
 * Generates and properly cleans up temporary files. Similar to {@link
 * File#createTempFile(java.lang.String, java.lang.String)}, this class provides
 * a static method to create temporary files. The temporary files will be
 * created in a special directory to be cleaned up the next time this class is
 * loaded by the JVM. This functionality is required because Win32 platforms
 * will not allow the JVM to delete files that are open. This causes problems
 * with items such as JARs that get opened by a URLClassLoader and can therefore
 * not be deleted by the JVM (including deleteOnExit).
 * 
 * The caller should not need to create an instance of this class, although it
 * is possible. Simply use the static methods to perform the required
 * operations. Note that all files created by this class should be considered as
 * deleted at JVM exit (although the actual deletion may be delayed). If
 * persistent temporary files are required, use {@linkjava.io.File} instead.
 * 
 * Refer to Sun bugs 4171239 and 4950148 for more details.
 */
public class TempFileManager {

    /**
     * The prefix for the temp directory in the system temp directory
     */
    private final static String TEMP_DIR_PREFIX = "tmp-mgr-";

    /**
     * The temp directory to generate all files in
     */
    private static File sTmpDir = null;
    
    /*
     * createTempFile(String prefix, String suffix)メソッドは、呼び出し元にとっ
     * てはFileクラスのメソッドと同様の動作をするだけのものですが、内部の処理は
     * 少し異なっています。
     * 
     * このマネージャは最初に、初期化されているかどうかのチェックをします。
     * 初期化されていない場合、マネージャはシステムのテンポラリディレクトリに
     * 基づいてディレクトリ名を生成し、そのロックファイルを作成します。
     * 最後に、テンポラリディレクトリを作成します。
     * 
     * こうした順序を踏むのは、ロックの処理前に、別のマネージャによって
     * テンポラリディレクトリが削除されるような競合状態を回避するためです。
     * 
     * ロックファイルは、終了時に削除対象として選ばれますが、JVMの中にはこれを
     * 参照しているものが存在しないので、これですべてうまく処理されるはずです。
     * このメソッドは最後に、ユーザーが要請したテンポラリファイルを、
     * 新しいディレクトリに作成します。
     * 
     */
    /**
     * Creates a temporary file in the proper directory to allow for cleanup
     * after execution. This method delegates to {@link
     * File#createTempFile(java.lang.String, java.lang.String, java.io.File)} so
     * refer to it for more documentation. Any file created using this method
     * should be considered as deleted at JVM exit; therefore, do not use this
     * method to create files that need to be persistent between application
     * runs.
     * 
     * @param prefix
     *            the prefix string used in generating the file name; must be at
     *            least three characters long
     * @param suffix
     *            the suffix string to be used in generating the file’s name;
     *            may be null, in which case the suffix ".tmp" will be used
     * @return an abstract pathname denoting a newly created empty file
     * @throws IOException
     *             if a file could not be created
     */
    public static File createTempFile(String prefix, String suffix)
            throws IOException {
        // Check to see if you have already initialized a temp directory
        // for this class.
        if (sTmpDir == null) {
            // Initialize your temp directory. You use the java temp directory
            // property, so you are sure to find the files on the next run.
            String tmpDirName = System.getProperty("java.io.tmpdir");
            File tmpDir = File.createTempFile(TEMP_DIR_PREFIX, ".tmp",
                    new File(tmpDirName));

            // Delete the file if one was automatically created by the JVM.
            // You are going to use the name of the file as a directory name,
            // so you do not want the file laying around.
            tmpDir.delete();

            // Create a lock before creating the directory so
            // there is no race condition with another application trying
            // to clean your temp dir.
            File lockFile = new File(tmpDirName, tmpDir.getName() + ".lck");
            lockFile.createNewFile();

            // Set the lock file to delete on exit so it is properly cleaned
            // by the JVM. This will allow the TempFileManager to clean
            // the overall temp directory next time.
            lockFile.deleteOnExit();

            // Make a temp directory that you will use for all future requests.
            if (!tmpDir.mkdirs()) {
                throw new IOException("Unable to create temporary directory:"
                        + tmpDir.getAbsolutePath());
            }

            sTmpDir = tmpDir;
        }

        // Generate a temp file for the user in your temp directory
        // and return it.
        return File.createTempFile(prefix, suffix, sTmpDir);
    }
    
    /*
     * TempFileManagerの次に重要なポイントは、静的な初期化ブロックです。
     * このブロックは、クラスが初めてロードされる際にJVMによって実行されます。
     * これはアプリケーションからのクラス要求に応じて、マネージャが即座に残留
     * テンポラリファイルを削除できるようにするためのものです。
     * 
     * このクリーンアップを実行するために、アプリケーションからマネージャに
     * 要求する必要はありません。
     *
     *
     * static{}ブロック内で、マネージャはテンポラリディレクトリのファイルリスト
     * を取得しますが、その際にファイルフィルタを用いて、このマネージャ用の
     * 接頭辞で始まるディレクトリだけを選択します。
     * 
     * 検出された個々のディレクトリについては、ロックファイルが存在するかどうか
     * のチェックが実行されます。
     * 
     * ロックファイルが存在しないディレクトリについては、ユーティリティが
     * recursiveDelete(FilerootDir)メソッドを用いて再帰的に削除します。
     * 
     * このマネージャを実装した場合、1つのインスタンスが存在すれば、
     * 共通のマネージャ実装を使っている他のアプリケーションについても、
     * それらのテンポラリファイルを削除します。
     * 
     * 個々のアプリケーションからTempFileManagerを使用する手順はごく簡単です。
     * 前回の実行後に放置されているファイルをこのテンポラリファイルマネージャ
     * で確実に削除するには、このクラスをJVMに強制的にロードさせます。
     * 
     * Class.forName(TempFileManager.class.getName());
     * 
     * 新しいテンポラリファイルを生成するとマネージャが自動的にロードされるので、
     * これは必須のステップではありません。
     * しかし、明示的にマネージャをロードしておくと、ドキュメンテーションの意味
     * では役に立ちます。新しくテンポラリファイルを作成する手順は、JFC APIを
     * 呼び出す場合とほぼ同一です。
     * 
     * File myTmp = TempFileManager.createTempFile("foo", ".bar");
     * 
     *  
     */
    /**
     * Static block used to clean up any old temp directories found -- the JVM
     * will run this block when a class loader loads the class.
     */
    static {
        System.out.println("テンポラリディレクトリのクリーンアップを開始");
        
        // Clean up any old temp directories by listing
        // all of the files, using a filter that will
        // return only directories that start with your
        // prefix.
        FileFilter tmpDirFilter = new FileFilter() {
            public boolean accept(File pathname) {
                return (pathname.isDirectory() && pathname.getName()
                        .startsWith(TEMP_DIR_PREFIX));
            }
        };

        // Get the system temp directory and filter the files.
        String tmpDirName = System.getProperty("java.io.tmpdir");
        File tmpDir = new File(tmpDirName);
        File[] tmpFiles = tmpDir.listFiles(tmpDirFilter);

        // Find all the files that do not have a lock by
        // checking if the lock file exists.
        for (int i = 0; i < tmpFiles.length; i++) {
            File tmpFile = tmpFiles[i];

            // Create a file to represent the lock and test.
            File lockFile = new File(tmpFile.getParent(), tmpFile.getName()
                    + ".lck");
            if (!lockFile.exists()) {
                // Delete the contents of the directory since
                // it is no longer locked.
                Logger.getLogger("default").log(
                        Level.FINE,
                        "TempFileManager::deleting old temp directory "
                                + tmpFile);

                try {
                    recursiveDelete(tmpFile);
                } catch (IOException ex) {
                    // You log at a fine level since not being able to delete
                    // the temp directory should not stop the application
                    // from performing correctly. However, if the application
                    // generates a lot of temp files, this could become
                    // a disk space problem and the level should be raised.
                    Logger.getLogger("default").log(
                            Level.INFO,
                            "TempFileManager::unable to delete "
                                    + tmpFile.getAbsolutePath());

                    // Print the exception.
                    ByteArrayOutputStream ostream = new ByteArrayOutputStream();
                    ex.printStackTrace(new PrintStream(ostream));

                    Logger.getLogger("default").log(Level.FINE,
                            ostream.toString());
                }
            }
//add 2006/10/26 kano start
            //アプリケーションダウン時にlockファイルが残ってしまう現象への対応
            //lockファイルが存在する場合も削除可能か試してみる
            else{
                System.out.println("ロックファイルが残っていますが削除してみます [" + lockFile.getName() +"]");
                try {
                    lockFile.delete();
                    recursiveDelete(tmpFile);
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
//add 2006/10/26 kano end
        }
    }
    
    public static void tmpClean(){
        
    }
    
    /**
     * Utility method to load the TempFileManager at any time and allow it to
     * clean the temporary files that may be left from previous instances
     * 
     * @param args
     *            command line arguments are currently not supported
     */
    public static void main(String[] args) {
        // Although the JVM will load the class in order to
        // run the main method, this gives a little clarity to
        // what is happening and why we want the main method.
        try {
            // This will load the TempFileManager, which will
            // cause the static block to execute, cleaning
            // any old temp files.
            Class.forName(TempFileManager.class.getName());
        } catch (ClassNotFoundException ex) {
            ex.printStackTrace();
        }
    }

    /**
     * Deletes all of the files in the given directory, recursing into any sub
     * directories found. Also deletes the root directory.
     * 
     * @param rootDir
     *            the root directory to be recursively deleted
     * @throws IOException
     *             if any file or directory could not be deleted
     */
    private static void recursiveDelete(File rootDir) throws IOException {
        // Select all the files
        File[] files = rootDir.listFiles();
        for (int i = 0; i < files.length; i++) {
            // If the file is a directory, we will
            // recursively call delete on it.
            if (files[i].isDirectory()) {
                recursiveDelete(rootDir);
            } else {
                // It is just a file so we are safe to
                // delete it
                if (!files[i].delete()) {
                    throw new IOException("Could not delete: "
                            + files[i].getAbsolutePath());
                }
            }
        }

        // Finally, delete the root directory now
        // that all of the files in the directory have
        // been properly deleted.
        if (!rootDir.delete()) {
            throw new IOException("Could not delete: "
                    + rootDir.getAbsolutePath());
        }
    }

    
}