/*
 * FileOperation class.
 *
 * Copyright (C) 2007 SATOH Takayuki All Rights Reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */
package ts.util.file;

import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.FileNotFoundException;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.LinkedList;
import ts.util.Duo;
import ts.util.text.StringSequence;

/**
 * ファイル操作クラス。
 * <br>
 * ファイルの一覧、コピー、削除、移動を実行するためのメソッドを用意する。
 *
 * @author  佐藤隆之. 
 * @version $Revision: 1.1.1.1 $, $Date: 2010-10-16 00:03:46 $
 */
public final class FileOperation
{
  /**
   * デフォルトコンストラクタ。
   */
  protected FileOperation()
  {}

  /**
   * 移動先やコピー先となるファイルの{@link java.io.File File}オブジェクトを
   * 取得する。
   *
   * @param  srcFile 移動対象のファイル。
   * @param  baseDir 元の基底ディレクトリ。
   * @param  destDir 移動先のディレクトリ。
   * @return 移動先やコピー先となる{@link java.io.File File}オブジェクト。
   * @throws IOException 処理中に入出力例外が発生した場合。
   * @throws AssertionError 引数がヌルの場合、又は移動対象のファイルが元の基底
   *        ディレクトリ以下のファイルでない場合（デバッグ・モードのみ）。
   */
  protected static File getDestinationFile(
    File srcFile, File baseDir, File destDir) throws IOException
  {
    assert (srcFile != null) : "@param:srcFile is null.";
    assert (baseDir != null) : "@param:baseDir is null.";
    assert (destDir != null) : "@param:destDir is null.";

    StringSequence srcPath = new StringSequence(srcFile.getCanonicalPath());
    assert srcPath.startsWith(baseDir.getCanonicalPath());

    StringSequence basePath = new StringSequence(
      baseDir.getCanonicalPath() + File.separator);

    if (srcPath.restLength() > basePath.restLength()) {
      return new File(destDir, srcPath.next(basePath.restLength()).substring());
    }
    else {
      return destDir;
    }
  }

  /**
   * 指定されたディレクトリを作成する。
   * <br>
   * 引数がヌルの場合は、そのままヌルを返す。
   *
   * @param  dir ディレクトリ。
   * @return 作成された最上位のディレクトリ。指定されたディレクトリが既に作成
   *           されていた場合はヌルが返される。
   * @throws IOException 作成時に例外が発生した場合。
   */
  private static File createDir(File dir) throws IOException
  {
    if (dir == null) {
      return null;
    }

    File topDir = null;
    for (File d = dir; d != null && ! d.exists(); d = d.getParentFile()) {
      topDir = d;
    }
    if (! dir.mkdirs()) {
      if (dir.exists()) {
        if (! dir.isDirectory()) {
          throw new FileAlreadyExistsException(dir.getPath());
        }
      }
      else {
        throw new IOException(dir.getPath());
      }
    }
    return topDir;
  }

  /** 
   * 指定されたパスのファイルを作成する。
   * <br>
   * 指定されたファイルの親ディレクトリが存在しない場合は、親ディレクトリを作成
   * する。
   * 指定されたパスのファイルが既に存在する場合は、例外をスローする。
   *
   * @param  path 作成されるファイルのパス。
   * @throws IOException ファイルの作成に失敗した場合。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）。
   */
  public static File createNewFile(String path)
    throws FileAlreadyExistsException, IOException
  {
    assert (path != null) : "@param:path is null.";

    return createNewFile(new File(path));
  }

  /** 
   * 指定されたファイルを作成する。
   * <br>
   * 指定されたファイルの親ディレクトリが存在しない場合は、親ディレクトリを
   * 作成する。
   * 指定されたファイルが既に存在する場合は、例外をスローする。
   *
   * @param  file 作成されるファイル。
   * @throws IOException ファイルの作成に失敗した場合。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）。
   */
  public static File createNewFile(File file)
    throws IOException
  {
    assert (file != null) : "@param:file is null.";

    File createdTopDir = createDir(file.getParentFile());

    if (! file.createNewFile()) {
      if (createdTopDir != null) {
        deleteInner(createdTopDir);
      }

      if (file.exists()) {
        throw new FileAlreadyExistsException(file.getPath());
      }
      else {
        throw new IOException(file.getPath());
      }
    }

    return file;
  }

  /**
   * 指定されたディレクトリの下に空のファイルをユニークな名前で生成する。
   * <br>
   * ファイル名には、指定された接頭語及び接尾語を使用する。
   * 接頭語には3文字以上が必要である。
   * 接尾語には、ヌルを含む任意の文字列が指定可能であり、ヌルが指定された場合は
   * <tt>".tmp"</tt>が使用される。
   * <br>
   * ディレクトリにヌルが指定された場合は、
   * システム依存の一時ファイルディレクトリが使用される。
   * <br>
   * このメソッドは内部で{@link java.io.File#createTempFile(String,String,File)
   * createTempFile(String,String,File)}メソッドを呼び出している。
   *
   * @param  prefix ファイル名に使用される接頭語。
   * @param  suffix ファイル名に使用される接尾語。
   * @param  dir ファイルが生成されるディレクトリ。
   * @return 新規に生成されたファイルの{@link java.io.File File}オブジェクト。
   * @throws NullPointerException 接頭語が3文字に満たない場合。
   * @throws IllegalArgumentException 接頭語が3文字に満たない場合。
   * @throws IOException ファイルが生成できなかった場合。
   * @throws SecurityException セキュリティマネージャによりファイルの生成が
   *           が許可されない場合。
   * @see java.io.File#createTempFile(String, String, File)
   */
  public static File createTempFile(String prefix, String suffix, File dir)
    throws IllegalArgumentException, IOException
  {
    return File.createTempFile(prefix, suffix, dir);
  }

  /**
   * 指定されたディレクトリの下に空のディレクトリをユニークな名前で生成する。
   * <br>
   * ファイル名には、指定された接頭語及び接尾語を使用する。
   * 接頭語には3文字以上が必要である。
   * 接尾語には、ヌルを含む任意の文字列が指定可能であり、ヌルが指定された場合は
   * 空文字列が使用される。
   * <br>
   * ディレクトリにヌルが指定された場合は、
   * システム依存の一時ファイルディレクトリが使用される。
   * <br>
   * このメソッドは内部で{@link java.io.File#createTempFile(String,String,File)
   * createTempFile(String,String,File)}メソッドを呼び出して、
   * ユニークなディレクトリ名を決定している。
   *
   * @param  prefix ファイル名に使用される接頭語。
   * @param  suffix ファイル名に使用される接尾語。
   * @param  dir ファイルが生成されるディレクトリ。
   * @return 新規に生成されたディレクトリの{@link java.io.File
   *           File}オブジェクト。
   * @throws IllegalArgumentException 接頭語ご3文字に満たない場合。
   * @throws IOException ディレクトリが生成できなかった場合。
   * @throws SecurityException セキュリティマネージャによりディレクトリの生成が
   *           が許可されない場合。
   * @see java.io.File#createTempFile(String, String, File)
   */
  public static File createTempDirectory(String prefix, String suffix, File dir)
    throws IllegalArgumentException, IOException
  {
    File file = File.createTempFile(prefix, suffix, dir);
    file.delete();
    file.mkdirs();
    return file;
  }

  /**
   * 指定されたディレクトリ直下のファイルの一覧を取得する。
   * <br>
   * 指定されたディレクトリが空の場合は、空のリストを返す。
   * 引数がディレクトリではない場合は、引数を格納したリストを返す。
   * 処理中にエラーが発生した場合は例外をスローする。
   *
   * @param  baseDir ベースディレクトリ。
   * @return ベースディレクトリ直下にあるファイルを格納したリスト。
   * @throws FileNotFoundException 指定されたディレクトリ又はファイルが
   *           見つからない場合。
   * @throws IOException 処理中に入出力エラーが発生した場合。
   * @throws SecurityException セキュリティマネージャによりファイルへの読込
   *           アクセスが許可されない場合。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ)。
   */
  public static List<File> list(File baseDir)
    throws FileNotFoundException, IOException
  {
    assert (baseDir != null) : "@param:baseDir is null.";

    if (! baseDir.exists()) {
      throw new FileNotFoundException(baseDir.getPath());
    }

    List<File> lst = new LinkedList<File>();

    File[] ret = baseDir.listFiles();
    if (ret == null) {
      if (! baseDir.isDirectory()) {
        lst.add(baseDir);
        return lst;
      }
      else {
        throw new IOException(baseDir.getPath());
      }
    }

    for (int i=0; i<ret.length; i++) {
      lst.add(ret[i]);
    }

    return lst;
  }

  /**
   * 指定されたディレクトリ直下のファイルのうち、フィルタの条件に該当する
   * ファイルの一覧を取得する。
   * <br>
   * 指定されたディレクトリが空の場合は、空のリストを返す。
   * フィルタの条件に該当するファイルが一つも存在しなかった場合は、空のリスト
   * を返す。
   * 引数がディレクトリでない場合は、引数のファイルをフィルタの条件で判定して
   * リストに格納して返す。
   * 処理中にエラーが発生した場合は例外をスローする。
   *
   * @param  baseDir ベース・ディレクトリ。
   * @param  filter ファイルの選別に使用されるフィルタ。
   * @return ベース・ディレクトリ直下にあるファイルを格納したリスト。
   * @throws FileNotFoundException 指定されたディレクトリ又はファイルが
   *           見つからない場合。
   * @throws IOException 処理中に入出力エラーが発生した場合。 
   * @throws SecurityException セキュリティ・マネージャによりファイルへの
   *           読込アクセスが許可されない場合。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）。
   */
  public static List<File> list(File baseDir, FileFilter filter)
    throws FileNotFoundException, IOException
  {
    assert (filter != null) : "@param:filter is null.";

    List<File> lst = list(baseDir);

    Iterator<File> it = lst.iterator();
    while (it.hasNext()) {
      if (! filter.accept(it.next())) {
        it.remove();
      }
    }
    return lst;
  }

  /**
   * 指定されたディレクトリ以下の全てのファイルの一覧を取得する。
   * <br>
   * 指定されたディレクトリ下にディレクトリがあれば、その下に格納されている
   * ファイルも一覧に含めて返す。
   * 指定されたディレクトリが空の場合は、空の配列を返す。
   * 引数がディレクトリでない場合は、引数を格納したリストを返す。
   * 処理中にエラーが発生した場合は例外をスローする。
   *
   * @param  baseDir ベースディレクトリ。
   * @return ベースオブジェクト以下にあるファイルの{@link java.io.File File}
   *           オブジェクトのリスト。
   * @throws FileNotFoundException 指定されたディレクトリ又はファイルが
   *           見つからない場合。
   * @throws IOException 処理中に入出力エラーが発生した場合。
   * @throws SecurityException セキュリティマネージャによりファイルへの読込
   *           アクセスが許可されない場合。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ)。
   */
  public static List<File> listRecursive(File baseDir)
    throws FileNotFoundException, IOException
  {
    assert (baseDir != null) : "@param:baseDir is null.";

    if (! baseDir.exists()) {
      throw new FileNotFoundException(baseDir.getPath());
    }

    List<File> lst = new LinkedList<File>();
    if (! baseDir.isDirectory()) {
      lst.add(baseDir);
      return lst;
    }

    return listInner(baseDir, lst);
  }

  /**
   * 指定されたディレクトリ以下のファイルのうち、フィルタの条件に該当する
   * ファイルの一覧を取得する。
   * <br>
   * 指定されたディレクトリ下にディレクトリがあれば、その下に格納されている
   * ファイルもフィルタの条件で判定して一覧に含めて返す。
   * フィルタの条件に該当するファイルが一つも存在しなかった場合は、空のリスト
   * を返す。
   * 指定されたディレクトリが空の場合は、空のリストを返す。
   * 引数がディレクトリでない場合は、引数のファイルをフィルタの条件で判定して
   * リストに格納して返す。
   * 処理中にエラーが発生した場合は例外をスローする。
   *
   * @param  baseDir ベースディレクトリ。
   * @param  filter ファイルの選別に使用するフィルタ。
   * @return ベースオブジェクト以下にあるファイルの{@link java.io.File File}
   *           オブジェクトのリスト。
   * @throws FileNotFoundException 指定されたディレクトリ又はファイルが
   *           見つからない場合。
   * @throws IOException 処理中に入出力エラーが発生した場合。
   * @throws SecurityException セキュリティマネージャによりファイルへの読込
   *           アクセスが許可されない場合。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ)。
   */
  public static List<File> listRecursive(File baseDir, FileFilter filter)
    throws FileNotFoundException, IOException
  {
    assert (filter != null) : "@param:filter is null.";

    List<File> lst = listRecursive(baseDir);
    Iterator<File> it = lst.iterator();
    while (it.hasNext()) {
      if (! filter.accept(it.next())) {
        it.remove();
      }
    }
    return lst;
  }

  /**
   * 指定されたファイルを削除する。
   * <br>
   * 指定されたファイルがディレクトリの場合、そのディレクトリが空ならば
   * 削除は成功する。
   *
   * @param  file 削除対象のファイル。
   * @throws FileNotFoundException 指定されたファイルが見つからない場合。
   * @throws DirectoryNotEmptyException 指定されたファイルが空のディレクトリで
   *           ない場合。
   * @throws IOException 指定されたファイルがロック中の場合、又は処理中に
   *           入出力エラーが発生した場合。
   * @throws SecurityException セキュリティマネージャによりファイルへの読込
   *           アクセス又は削除アクセスが許可されない場合。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ)。
   */
  public static void delete(File file)
    throws FileNotFoundException, DirectoryNotEmptyException, IOException
  {
    assert (file != null) : "@param:file is null.";

    if (! file.delete()) {
      if (! file.exists()) {
        throw new FileNotFoundException(file.getPath());
      }

      if (file.isDirectory()) {
        if (file.list().length > 0)
          throw new DirectoryNotEmptyException(file.getPath());
      }

      throw new IOException(file.getPath());
    }
  }

  /**
   * 指定されたファイルがファイルがフィルタの条件に合致する場合に削除する。
   * <br>
   * 指定されたファイルがディレクトリの場合、そのディレクトリが空でないならば
   * 削除しない。
   *
   * @param  file 削除対象のファイル。
   * @param  filter 削除対象のファイルの選別に使用するフィルタ。
   * @throws FileNotFoundException 指定されたファイルが見つからない場合。
   * @throws DirectoryNotEmptyException 指定されたファイルが空のディレクトリで
   *           ない場合。
   * @throws IOException 処理中に入出力エラーが発生した場合。
   * @throws SecurityException セキュリティマネージャによりファイルへの読込
   *           アクセス又は削除アクセスが許可されない場合。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ)。
   */
  public static void delete(File file, FileFilter filter)
    throws FileNotFoundException, DirectoryNotEmptyException, IOException
  {
    assert (file != null) : "@param:file is null.";
    assert (filter != null) : "@param:filter is null.";

    if (! file.exists()) {
      throw new FileNotFoundException(file.getPath());
    }

    if (filter.accept(file)) {
      delete(file);
    }
  }

  /**
   * 指定されたファイルを削除する。
   * <br>
   * 指定されたファイルがディレクトリの場合、そのディレクトリ以下の全ての
   * ファイルを削除する。
   *
   * @param  file 削除対象のファイル。
   * @throws FileNotFoundException 指定されたファイルが見つからない場合。
   * @throws IOException 処理中に入出力エラーが発生した場合。
   * @throws SecurityException セキュリティマネージャによりファイルへの読込
   *           アクセス又は削除アクセスが許可されない場合。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ)。
   */
  public static void deleteRecursive(File file)
    throws FileNotFoundException, IOException
  {
    assert (file != null) : "@param:file is null.";

    if (! file.exists()) {
      throw new FileNotFoundException(file.getPath());
    }

    File tmpDir = createTempDirectory("._del", "", file.getParentFile());
    try {
      File dstDir = new File(tmpDir, file.getName());
      if (! file.renameTo(dstDir)) {
        throw new IOException(file.getPath());
      }
    }
    finally {
      deleteInner(tmpDir);
    }
  }

  /**
   * 指定されたファイルがフィルタの条件に合致する場合、そのファイルを削除する。
   * <br>
   * 指定されたファイルがディレクトリの場合は、そのディレクトリ以下のファイル
   * をフィルタの条件で判定して削除する。
   *
   * @param  file 削除対象のファイル。
   * @param  filter 削除対象のファイルの選別に使用するフィルタ。
   * @throws FileNotFoundException 指定されたファイルが見つからない場合。
   * @throws IOException 処理中に入出力エラーが発生した場合。
   * @throws SecurityException セキュリティマネージャによりファイルへの読込
   *           アクセス又は削除アクセスが許可されない場合。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ)。
   */
  public static void deleteRecursive(File file, FileFilter filter)
    throws FileNotFoundException, IOException
  {
    assert (file != null) : "@param:file is null.";
    assert (filter != null) : "@param:filter is null.";

    if (! file.exists()) {
      throw new FileNotFoundException(file.getPath());
    }

    List<File> lst = new LinkedList<File>();
    lst.add(file);
    if (file.isDirectory()) {
      listInner(file, lst);
    }

    File tmpDir = createTempDirectory("._del", "", file.getParentFile());
    File dstDir = new File(tmpDir, file.getName());
    boolean complete = false;
    try {
      for (File f : lst) {
        if (! f.exists()) { // already moved with the parent directory.
          continue;
        }
        if (! filter.accept(f)) {
          continue;
        }
        File d = getDestinationFile(f, file, dstDir);
        File p = d.getParentFile();
        if (p != null) {
          p.mkdirs();
        }
        if (! f.renameTo(d)) {
          throw new IOException(f.getPath());
        }
      }
      complete = true;
    }
    finally {
      if (! complete) {
        moveInner(dstDir, file);
      }
      deleteInner(tmpDir);
    }
  }

  /**
   * 第１引数のファイルを第２引数のファイルに移動する。
   * <br>
   * 第１引数が通常のファイルの場合はそれを第２引数に移動する。
   * 第１引数がディレクトリの場合はそのディレクトリを下位のファイルと一緒に移動
   * する。
   * 但し、第２引数が既に存在する場合は、それが通常のファイルであれディレクトリ
   * であれ、移動を行わずに例外をスローする。
   *
   * @param  src 移動元のファイル。
   * @param  dst 移動先のファイル。
   * @throws FileNotFoundException 移動元のファイルが存在しない場合。
   * @throws FileAlreadyExistsExcdeption 移動先のファイルが既に存在している
   *           場合。
   * @throws IOException 処理中に入出力エラーが発生した場合。
   * @throws SecurityException セキュリティマネージャによりファイルへの読込
   *           アクセス、又はファイル書込が許可されない場合。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ)。
   */
  public static void move(File src, File dst)
    throws FileNotFoundException, FileAlreadyExistsException, IOException
  {
    assert (src != null) : "@param:src is null.";
    assert (dst != null) : "@param:dst is null.";

    if (! src.exists()) {
      throw new FileNotFoundException(src.getPath());
    }

    boolean complete = false;

    File createdTopDir = createDir(dst.getParentFile());
    File tmpDir = null, tmpSrcDir = null;
    try {
      if (src.renameTo(dst)) {
        complete = true;
        return;
      }
      else if (dst.exists()) {
        throw new FileAlreadyExistsException(dst.getPath());
      }
      else {
        // UNIX環境では同一ファイル・システムでないとrenameToに失敗
        tmpDir = createTempDirectory("._mov", "", src.getParentFile());
        tmpSrcDir = new File(tmpDir, src.getName());
        if (! src.renameTo(tmpSrcDir)) {
          throw new IOException(src.getPath());
        }
        copyInner(tmpSrcDir, dst);
        complete = true;
      }
    }
    finally {
      if (! complete) {
        if (tmpSrcDir != null) {
          if (tmpSrcDir != null) {
            tmpSrcDir.renameTo(src);
          }
          if (createdTopDir != null) {
            deleteInner(createdTopDir);
          }
        }
        if (tmpDir != null) {
          deleteInner(tmpDir);
        }
      }
    }
  }

  /**
   * 第１引数のファイルがフィルタに該当する場合に、第２引数のファイルに
   * 移動する。
   * <br>
   * 第１引数が通常のファイルの場合はそれを第２引数に移動する。
   * 第１引数がディレクトリの場合はそのディレクトリを下位のファイルと一緒に移動
   * する。
   * 但し、第２引数が既に存在する場合は、それが通常のファイルであれディレクトリ
   * であれ、移動を行わずに例外をスローする。
   *
   *
   * @param  src 移動元のファイル。
   * @param  dst 移動先のファイル。
   * @param  filter 移動対象のファイルの選別に使用するフィルタ。
   * @throws FileNotFoundException 移動元のファイルが存在しない場合。
   * @throws FileAlreadyExistsExcdeption 移動先のファイルが既に存在している
   *           場合。
   * @throws IOException 処理中に入出力エラーが発生した場合。
   * @throws SecurityException セキュリティマネージャによりファイルへの読込
   *           アクセス、又はファイル書込が許可されない場合。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ)。
   */
  public static void move(File src, File dst, FileFilter filter)
    throws FileNotFoundException, FileAlreadyExistsException, IOException
  {
    assert (src != null) : "@param:src is null.";
    assert (dst != null) : "@param:dst is null.";
    assert (filter != null) : "@param:filter is null.";

    if (! src.exists()) {
      throw new FileNotFoundException(src.getPath());
    }

    if (! filter.accept(src)) {
      return;
    }

    boolean complete = false;

    File createdTopDir = createDir(dst.getParentFile());
    File tmpDir = null, tmpSrcDir = null;
    try {
      if (src.renameTo(dst)) {
        complete = true;
        return;
      }
      else if (dst.exists()) {
        throw new FileAlreadyExistsException(dst.getPath());
      }
      else {
        // UNIX環境では同一ファイル・システムでないとrenameToに失敗
        tmpDir = createTempDirectory("._mov", "", src.getParentFile());
        tmpSrcDir = new File(tmpDir, src.getName());
        if (! src.renameTo(tmpSrcDir)) {
          throw new IOException(src.getPath());
        }
        copyInner(tmpSrcDir, dst);
        complete = true;
      }
    }
    finally {
      if (! complete) {
        if (tmpSrcDir != null) {
          tmpSrcDir.renameTo(src);
        }
        if (createdTopDir != null) {
          deleteInner(createdTopDir);
        }
      }
      if (tmpDir != null) {
        deleteInner(tmpDir);
      }
    }
  }

  /**
   * 第１引数のファイルを第２引数のファイルに移動する。
   * <br>
   * 第１引数がディレクトリの場合は、それ以下のファイルも全て移動する。
   * <br>
   * 移動先のファイルが既に存在している場合は、そのファイルがディレクトリでない
   * ならば例外をスローし、ディレクトリならばその下のファイルの移動を実行する。
   * <br>
   * 移動中に例外が発生した場合は、移動したファイルを元に戻す。
   *
   * @param  src 移動元のファイル。
   * @param  dst 移動先のファイル。
   * @throws FileNotFoundException 移動元のファイルが存在しない場合。
   * @throws FileAlreadyExistsExcdeption 移動先のファイルが既に存在している
   *           場合（通常ファイルのみ）。
   * @throws IOException 処理中に入出力エラーが発生した場合。
   * @throws SecurityException セキュリティマネージャによりファイルへの読込
   *           アクセス、又はファイル書込が許可されない場合。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ)。
   */
  public static void moveRecursive(File src, File dst)
    throws FileNotFoundException, FileAlreadyExistsException,
           DirectoryNotEmptyException, IOException
  {
    moveInner(src, dst);
  }

  /**
   * 第１引数のファイルがフィルタの条件に該当する場合に、第２引数のファイルに
   * 移動する。
   * <br>
   * 第１引数がディレクトリの場合、それ以下のファイルでフィルタの条件に該当する
   * ものも移動する。
   * <br>
   * 移動先のファイルが既に存在している場合は、そのファイルがディレクトリでない
   * ならば例外をスローし、ディレクトリならばその下のファイルの移動を実行する。
   * <br>
   * 移動中に例外が発生した場合は、移動したファイルを元に戻す。
   *
   * @param  src 移動元のファイル。
   * @param  dst 移動先のファイル。
   * @throws FileNotFoundException 移動元のファイルが存在しない場合。
   * @throws FileAlreadyExistsExcdeption 移動先のファイルが既に存在している
   *           場合（通常ファイルのみ）。
   * @throws IOException 処理中に入出力エラーが発生した場合。
   * @throws SecurityException セキュリティマネージャによりファイルへの読込
   *           アクセス、又はファイル書込が許可されない場合。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ)。
   */
  public static void moveRecursive(File src, File dst, FileFilter filter)
    throws FileNotFoundException, FileAlreadyExistsException,
           DirectoryNotEmptyException, IOException
  {
    moveInner(src, dst, filter);
  }

  /**
   * 第１引数のファイルを第２引数のファイルにコピーする。
   * <br>
   * 第１引数のファイルが存在しない場合は例外をスローする。
   * 第２引数のファイルが既に存在する場合は例外をスローする。
   * 第１引数のファイルがディレクトリの場合、そのディレクトリが空でない場合は
   * 例外をスローする。
   *
   * @param  src コピー元のファイル。
   * @param  dst コピー先のファイル。
   * @throws FileNotFoundException コピー元のファイルが存在しない場合。
   * @throws DirectoryNotEmptyException コピー元のディレクトリが空でない場合。
   * @throws FileAlreadyExistsException コピー先のファイルが既に存在する場合。
   * @throws IOException 処理中に入出力エラーが発生した場合。
   * @throws SecurityException セキュリティマネージャによりファイルへの読込
   *           アクセス、存在確認アクセス、ディレクトリ生成、ファイル書込が
   *           許可されない場合。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ)。
   */
  public static void copy(File src, File dst)
    throws FileNotFoundException, DirectoryNotEmptyException,
           FileAlreadyExistsException, IOException
  {
    assert (src != null) : "@param:src is null.";
    assert (dst != null) : "@param:dst is null.";

    if (! src.exists()) {
      throw new FileNotFoundException(dst.getPath());
    }
    else if (dst.exists()) {
      throw new FileAlreadyExistsException(dst.getPath());
    }
    
    if (! src.isDirectory()) {
      boolean complete = false;
      File createdTopDir = createDir(dst.getParentFile());
      try {
        copyFile(src, dst);
        complete = true;
      }
      finally {
        if (! complete) {
          if (createdTopDir != null) {
            deleteInner(createdTopDir);
          }
        }
      }
    }
    else {
      if (src.list().length > 0) {
        throw new DirectoryNotEmptyException(src.getPath());
      }
      dst.mkdirs();
    }
  }

  /**
   * 第１引数のファイルがフィルタの条件に合致する場合に、第２引数のファイルに
   * コピーする。
   * <br>
   * 第１引数のファイルが存在しない場合は例外をスローする。
   * 第２引数のファイルが既に存在する場合は例外をスローする。
   * 第１引数のファイルがディレクトリの場合、そのディレクトリが空でない場合は
   * 例外をスローする。
   *
   * @param  src コピー元のファイル。
   * @param  dst コピー先のファイル。
   * @param  filter コピー対象のファイルの選別に使用するフィルタ。
   * @throws FileNotFoundException コピー元のファイルが存在しない場合。
   * @throws DirectoryNotEmptyException コピー元のディレクトリが空でなかった
   *           場合。
   * @throws FileAlreadyExistsException コピー先のファイルが既に存在している
   *           場合。
   * @throws IOException 処理中に入出力エラーが発生した場合。
   * @throws SecurityException セキュリティマネージャによりファイルへの読込
   *           アクセス、存在確認アクセス、ディレクトリ生成、ファイル書込が
   *           許可されない場合。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ)。
   */
  public static void copy(File src, File dst, FileFilter filter)
    throws FileNotFoundException, DirectoryNotEmptyException, 
           FileAlreadyExistsException, IOException
  {
    assert (src != null) : "@param:src is null.";
    assert (dst != null) : "@param:dst is null.";
    assert (filter != null) : "@param:filter is null.";

    if (! src.exists()) {
      throw new FileNotFoundException(dst.getPath());
    }

    if (! filter.accept(src)) {
      return;
    }

     if (dst.exists()) {
      throw new FileAlreadyExistsException(dst.getPath());
    }
    
    if (! src.isDirectory()) {
      boolean complete = false;
      File createdTopDir = createDir(dst.getParentFile());
      try {
        copyFile(src, dst);
        complete = true;
      }
      finally {
        if (! complete) {
          if (createdTopDir != null) {
            deleteInner(createdTopDir);
          }
        }
      }
    }
    else {
      if (src.list().length > 0) {
        throw new DirectoryNotEmptyException(src.getPath());
      }
      dst.mkdirs();
    }
  }

  /**
   * 第１引数のファイルを第２引数のファイルにコピーする。
   * <br>
   * 第１引数のファイルがディレクトリの場合、そのディレクトリ以下のファイルも
   * 全てコピーする。
   * <br>
   * 第１引数のファイルが存在しない場合は例外をスローする。
   * 第２引数のファイルが既に存在する場合は例外をスローする。
   *
   * @param  src コピー元のファイル。
   * @param  dst コピー先のファイル。
   * @throws FileNotFoundException コピー元のファイルが存在しなかった場合。
   * @throws FileAlreadyExistsException コピー先の、ディレクトリでないファイルが
   *           既に存在している場合。
   * @throws IOException 処理中に入出力エラーが発生した場合。
   * @throws SecurityException セキュリティマネージャによりファイルへの読込
   *           アクセス、存在確認アクセス、ディレクトリ生成、ファイル書込が
   *           許可されない場合。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ)。
   */
  public static void copyRecursive(File src, File dst)
    throws FileNotFoundException, FileAlreadyExistsException, IOException
  {
    copyInner(src, dst);
  }

  /**
   * コピー元のファイルがフィルタの条件に合致する場合に、コピー先にコピーする。
   * <br>
   * 第１引数のファイルがディレクトリの場合、そのディレクトリ以下のファイルを
   * フィルタの条件で判定してコピーする。
   *
   * @param  src コピー元のファイル。
   * @param  dst コピー先のファイル。
   * @param  filter コピー対象のファイルの選別に使用するフィルタ。
   * @throws FileNotFoundException コピー元のファイルが存在しなかった場合。
   * @throws FileAlreadyExistsException コピー先のファイルが既に存在している
   *           場合。
   * @throws IOException 処理中に入出力エラーが発生した場合。
   * @throws SecurityException セキュリティマネージャによりファイルへの読込
   *           アクセス、存在確認アクセス、ディレクトリ生成、ファイル書込が
   *           許可されない場合。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ)。
   */
  public static void copyRecursive(File src, File dst, FileFilter filter)
    throws FileNotFoundException, FileAlreadyExistsException, IOException
  {
    copyInner(src, dst, filter);
  }


  /**
   * 指定されたディレクトリ以下の全てのファイルを格納したリストを取得する。
   * <br>
   * リストに格納されるファイルの順序は、親ディレクトリの方が、その下のファイル
   * よりも先に格納される。
   * <br>
   * 指定されたファイルがディレクトリでなかった場合は、何もせず引数のリストを
   * そのまま返す。
   * <br>
   * 指定されたディレクトリが空の場合は、引数のリストに何も追加せずそのまま
   * 返す。
   *
   * @param  dir ディレクトリ。
   * @param  fileLst ファイルの一覧を格納するリスト。
   * @return ファイルの一覧を格納したリスト。引数の<code>fileLst</code>と同じ
   *           である。
   * @throws IOException 処理中に入出力エラーが発生した場合。
   * @throws SecurityException セキュリティマネージャによりファイルへの読込
   *           アクセスが許可されない場合。
   * @throws AssertionError 引数がヌルの場合、
   *           又は<code>dir</code>が存在しない場合、
   *           又は<code>dir</code>がディレクトリでない場合
   *           （デバッグ・モードのみ)。
   */
  private static List<File> listInner(File dir, List<File> fileLst)
    throws IOException
  {
    assert (dir != null) : "@param:dir is null.";
    assert (fileLst != null) : "@param:fileLst is null.";
    assert (dir.exists()) : "@param:dir does not exist.";

    if (! dir.isDirectory()) {
      return fileLst;
    }
    
    File[] files = dir.listFiles();
    if (files == null) {
      throw new IOException(dir.getPath());
    }

    for (File f : files) {
      fileLst.add(f);
      listInner(f, fileLst);
    }
    return fileLst;
  }

  /**
   * 指定されたディレクトリ以下の全てのファイルを削除する。
   * <br>
   * 指定されたファイルがディレクトリでない場合も削除する。
   *
   * @param  dir ディレクトリ。
   * @throws IOException 処理中に入出力エラーが発生した場合。
   * @throws SecurityException セキュリティマネージャによりファイルへの読込
   *           アクセス又は削除アクセスが許可されない場合。
   * @throws AssertionError 引数がヌルの場合、
   *           又は<code>dir</code>がディレクトリでない場合
   *          （デバッグ・モードのみ)。
   */
  private static void deleteInner(File dir) throws IOException
  {
    assert (dir != null) : "@param:dir is null.";
    assert (dir.exists()) : "@param:dir does not exist.";
    
    if (dir.isDirectory()) {
      File[] files = dir.listFiles();
      if (files == null) {
        throw new IOException(dir.getPath());
      }

      for (int i=0; i<files.length; i++) {
        if (files[i].isDirectory()) {
          deleteInner(files[i]);
        }
        else {
          if (! files[i].delete()) {
            throw new IOException(dir.getPath());
          }
        }
      }
    }

    if (! dir.delete()) {
      throw new IOException(dir.getPath());
    }
  }

  /**
   * 指定されたディレクトリ以下の全てのファイルを移動する。
   * <br>
   * 移動先のファイルが既に存在している場合は、そのファイルがディレクトリでない
   * ならば例外をスローし、ディレクトリならばその下のファイルの移動を実行する。
   * <br>
   * 移動中に例外が発生した場合は、移動したファイルを元に戻す。
   *
   * @param  srcFile 移動元のファイル。
   * @param  dstFile 移動先のファイル。
   *
   * @throws FileAlreadyExistsException 移動先のファイルが既に存在する場合。
   * @throws FileNotFoundException 移動元のファイルが存在しない場合。
   * @throws IOException 移動中に例外が発生した場合。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）。
   */
  private static void moveInner(File src, File dst) throws IOException
  {
    assert (src != null) : "@param:src is null.";
    assert (dst != null) : "@param:dst is null.";

    if (! src.exists()) {
      throw new FileNotFoundException(src.getPath());
    }

    boolean complete = false;

    File createdTopDir = createDir(dst.getParentFile());
    File tmpDir = null, tmpSrcDir = null;
    LinkedList<Duo<File,File>> rollbackLst = new LinkedList<Duo<File,File>>();
    try {
      if (src.renameTo(dst)) {
        complete = true;
        return;
      }

      if (dst.exists()) {
        if (! src.isDirectory()) {
          throw new FileAlreadyExistsException(dst.getPath());
        }
        else if (! dst.isDirectory()) {
          throw new FileAlreadyExistsException(dst.getPath());
        }
      }
      else {
        // UNIX環境では同一ファイル・システムでないとrenameToに失敗
        if (! src.isDirectory()) {
          copyFile(src, dst);
          rollbackLst.addFirst(new Duo<File,File>(null, dst));
          if (! src.delete()) {
            throw new IOException(src.getPath());
          }
          complete = true;
          return;
        }
        else {
          if (! dst.mkdir()) {
            throw new IOException(dst.getPath());
          }
          rollbackLst.addFirst(new Duo<File,File>(null, dst));
        }
      }

      tmpDir = createTempDirectory("._mov", "", src.getParentFile());
      tmpSrcDir = new File(tmpDir, src.getName());
      if (! src.renameTo(tmpSrcDir)) {
        throw new IOException(src.getPath());
      }

      for (File srcChild : listInner(tmpSrcDir, new LinkedList<File>())) {
        File dstChild = getDestinationFile(srcChild, tmpSrcDir, dst);

        if (! srcChild.exists()) {  // already moved with parent directory.
          continue;
        }

        if (srcChild.renameTo(dstChild)) {
          rollbackLst.addFirst(new Duo<File,File>(srcChild, dstChild));
          continue;
        }
        else if (dstChild.exists()) {
          if (! srcChild.isDirectory()) {
            throw new FileAlreadyExistsException(dstChild.getPath());
          }
          else if (! dstChild.isDirectory()) {
            throw new FileAlreadyExistsException(dstChild.getPath());
          }
        }
        else {
          // UNIX環境では同一ファイル・システムでないとrenameToに失敗
          if (!srcChild.isDirectory()) {
            copyFile(srcChild, dstChild);
          }
          else {
            if (! dstChild.mkdir()) {
              throw new IOException(dst.getPath());
            }
          }
          rollbackLst.addFirst(new Duo<File,File>(null, dstChild));
        }
      }
      complete = true;
    } 
    finally {
      if (!complete) {
        for (Duo<File,File> duo : rollbackLst) {
          if (duo.getFirst() == null) {
            duo.getSecond().delete();
          }
          else {
            duo.getSecond().renameTo(duo.getFirst());
          }
        }
        if (tmpSrcDir != null) {
          tmpSrcDir.renameTo(src);
        }
        if (createdTopDir != null) {
          deleteInner(createdTopDir);
        }
      }
      if (tmpDir != null) {
        deleteInner(tmpDir);
      }
    }
  }

  /**
   * 指定されたディレクトリ以下のファイルのうち、フィルタに該当するのファイルを
   * 移動する。
   * <br>
   * 移動先のファイルが既に存在している場合は、そのファイルがディレクトリで
   * ないならば例外をスローし、ディレクトリならばその下のファイルの移動を
   * 再起的に実行する。
   * <br>
   * 移動中に例外が発生した場合は、移動したファイルを元に戻す。
   *
   * @param  srcFile 移動元のディレクトリ。
   * @param  dstFile 移動先のディレクトリ。
   * @param  filter 移動対象のファイルの選別に使用するフィルタ。
   * @throws IOException 処理中に入出力エラーが発生した場合。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）。
   */
  private static void moveInner(File src, File dst, FileFilter filter)
    throws IOException
  {
    assert (src != null) : "@param:src is null.";
    assert (dst != null) : "@param:dst is null.";
    assert (filter != null) : "@param:filter is null.";

    if (! src.exists()) {
      throw new FileNotFoundException(src.getPath());
    }

    if (filter.accept(src)) {
      moveInner(src, dst);
      return;
    }

    boolean complete = false;

    File createdTopDir = createDir(dst.getParentFile());
    File tmpDir = null, tmpSrcDir = null;
    LinkedList<Duo<File,File>> rollbackLst = new LinkedList<Duo<File,File>>();
    try {
      List<File> srcChildLst = listRecursive(src, filter);

      tmpDir = createTempDirectory("._mov", "", src.getParentFile());
      tmpSrcDir = new File(tmpDir, src.getName());
      for (File srcChild : srcChildLst) {
        if (! srcChild.exists()) {  // already moved with parent directory.
          continue;
        }

        File tmpSrcChild = getDestinationFile(srcChild, src, tmpSrcDir);
        createDir(tmpSrcChild.getParentFile());

        if (! srcChild.renameTo(tmpSrcChild)) {
          throw new IOException(srcChild.getPath());
        }
        rollbackLst.addFirst(new Duo<File,File>(srcChild, tmpSrcChild));
      }

      for (File tmpSrcChild : listInner(tmpSrcDir, new LinkedList<File>())) {
        File dstChild = getDestinationFile(tmpSrcChild, tmpSrcDir, dst);

        if (! tmpSrcChild.exists()) {  // already moved with parent directory.
          continue;
        }

        File dir = createDir(dstChild.getParentFile());
        if (dir != null) {
          rollbackLst.addFirst(new Duo<File,File>(null, dir));
        }

        if (tmpSrcChild.renameTo(dstChild)) {
          rollbackLst.addFirst(new Duo<File,File>(tmpSrcChild, dstChild));
          continue;
        }
        else if (dstChild.exists()) {
          if (! tmpSrcChild.isDirectory()) {
            throw new FileAlreadyExistsException(dstChild.getPath());
          }
          else if (! dstChild.isDirectory()) {
            throw new FileAlreadyExistsException(dstChild.getPath());
          }
        }
        else {
          // UNIX環境では同一ファイル・システムでないとrenameToに失敗
          if (! tmpSrcChild.isDirectory()) {
            copyFile(tmpSrcChild, dstChild);
          }
          else {
            if (! dstChild.mkdir()) {
              throw new IOException(dst.getPath());
            }
          }
          rollbackLst.addFirst(new Duo<File,File>(null, dstChild));
        }
      }
      complete = true;
    }
    finally {
      if (! complete) {
        for (Duo<File,File> duo : rollbackLst) {
          if (duo.getFirst() == null) {
            duo.getSecond().delete();
          }
          else {
            duo.getSecond().renameTo(duo.getFirst());
          }
        }
        if (tmpSrcDir != null) {
          tmpSrcDir.renameTo(src);
        }
        if (createdTopDir != null) {
          deleteInner(createdTopDir);
        }
      }
      if (tmpDir != null) {
        deleteInner(tmpDir);
      }
    }
  }

  /**
   * ファイルの移動やコピーに失敗した原因を調べ、それに対する例外をスローする。
   *
   * @param  src 移動・コピー元のファイル。
   * @param  dst 移動・コピー先のファイル。
   * @throws IOException 失敗した原因に対応する例外。
   */
  private static void checkFailureReason(File src, File dst)
    throws IOException
  {
    if (! src.exists()) {
      throw new FileNotFoundException(src.getPath());
    }
    else if (dst.exists()) {
      if (src.isDirectory() != dst.isDirectory()) {
        throw new FileAlreadyExistsException(dst.getPath());
      }
      else if (! src.isDirectory()) {
        throw new FileAlreadyExistsException(dst.getPath());
      }
    }
  }

  /**
   * ファイルの実体をコピーする。
   * <br>
   * コピー先のファイルが既に存在する場合は、上書きする。
   *
   * @param  src コピー元のファイル。
   * @param  dst コピー先のファイル。
   * @throws FileNotFoundException コピー元のファイルが存在しないか、普通の
   *          ファイルではなくディレクトリであるか、または何らかの理由で開く
   *          ことができない場合。
   * @throws IOException コピー中に例外が発生した場合。
   * @throws AssertionError 引数がヌルの場合、又はコピー元のファイルに
   *           ディレクトリが指定された場合（デバッグ・モードのみ）。
   */
  private static void copyFile(File src, File dst)
    throws FileNotFoundException, IOException
  {
    assert (src != null) : "@param:src is null.";
    assert (dst != null) : "@param:dst is null.";
    assert (! src.isDirectory()) : "@param:src is a directory.";

    boolean dstCreated = !dst.exists();
    boolean complete = false;

    FileInputStream fis = null;
    FileOutputStream fos = null;
    try {
      fis = new FileInputStream(src);
      fos = new FileOutputStream(dst);

      int n;
      byte[] bytes = new byte[1024];
      while ((n = fis.read(bytes, 0, bytes.length)) > 0) {
        fos.write(bytes, 0, n);
      }

      complete = true;
    }
    finally {
      if (fos != null) {
        try {
          fos.close();
        }
        catch (Exception e) {}
      }
      if (fis != null) {
        try {
          fis.close();
        }
        catch (Exception e) {}
      }

      if (! complete && dstCreated) {
        dst.delete();
      }
    }
  }

  /**
   * 指定されたディレクトリ以下の全てのファイルをコピーする。
   * <br>
   * コピー先のファイルが既に存在している場合は、そのファイルがディレクトリで
   * ないならば例外をスローし、ディレクトリならばその下のファイルの移動を
   * 再起的に実行する。
   *
   * @param  src コピー元のディレクトリ。
   * @param  dst コピー先のディレクトリ。
   * @throws IOException 処理中に入出力エラーが発生した場合。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ)。
   */
  private static void copyInner(File src, File dst) throws IOException
  {
    assert (src != null) : "@param:src is null.";
    assert (dst != null) : "@param:dst is null.";

    checkFailureReason(src, dst);

    boolean complete = false;
    LinkedList<File> rollbackLst = new LinkedList<File>();
    File createdTopDir = null;
    try {
      createdTopDir = createDir(dst.getParentFile());

      List<File> fL = new LinkedList<File>();
      fL.add(src);
      if (src.isDirectory()) {
        listInner(src, fL);
      }

      for (File f0 : fL) {
        File f1 = getDestinationFile(f0, src, dst);

        checkFailureReason(f0, f1);

        if (! f0.isDirectory()) {
          copyFile(f0, f1);
          rollbackLst.addFirst(f1);
        }
        else if (! f1.exists()) {
          f1.mkdir();
          rollbackLst.addFirst(f1);
        }
      }
      complete = true;
    }
    finally {
      if (! complete) {
        if (createdTopDir != null) {
          deleteInner(createdTopDir);
        }
        else {
          for (File f1 : rollbackLst) {
            f1.delete();
          }
        }
      }//if (! complete)
    }
  }

  /**
   * 指定されたディレクトリ以下のファイルの内、フィルタに該当するのファイルを
   * コピーする。
   * <br>
   * コピー先のファイルが既に存在している場合は、そのファイルがディレクトリで
   * ないならば例外をスローし、ディレクトリならばその下のファイルの移動を
   * 再起的に実行する。
   *
   * @param  src コピー元のディレクトリ。
   * @param  dst コピー先のディレクトリ。
   * @param  filter コピー対象のファイルの選別に使用するフィルタ。
   * @throws IOException 処理中に入出力エラーが発生した場合。
   */
  private static void copyInner(File src, File dst, FileFilter filter)
    throws IOException
  {
    assert (src != null) : "@param:src is null.";
    assert (dst != null) : "@param:dst is null.";
    assert (filter != null) : "@param:filter is null.";

    checkFailureReason(src, dst);

    boolean complete = false;
    LinkedList<File> rollbackLst = new LinkedList<File>();

    try {
      List<File> fL = new LinkedList<File>();
      fL.add(src);
      if (src.isDirectory()) {
        listInner(src, fL);
      }

      for (File f0 : fL) {
        if (! filter.accept(f0)) {
          continue;
        }

        File f1 = getDestinationFile(f0, src, dst);

        checkFailureReason(f0, f1);

        if (! f0.isDirectory()) {
          File d = createDir(f1.getParentFile());
          if (d != null) {
            rollbackLst.addFirst(d);
          }
          copyFile(f0, f1);
          rollbackLst.addFirst(f1);
        }
        else if (! f1.exists()) {
          File d = createDir(f1);
          if (d != null) {
            rollbackLst.addFirst(d);
          }
        }
      }
      complete = true;
    }
    finally {
      if (! complete) {
        for (File f1 : rollbackLst) {
          if (f1.exists()) {
            deleteInner(f1);
          }
        }
      }//if (! complete)
    }
  }
}

