/*
 * Copyright (c) 2017 The openGion Project.
 *
 * 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 org.opengion.fukurou.fileexec;

import java.util.List;
import java.util.function.Consumer;

import java.io.File;
import java.io.PrintWriter;
import java.io.BufferedReader;
import java.io.FileInputStream ;
import java.io.InputStreamReader ;
import java.io.IOException;

import java.nio.file.Path;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.FileVisitor;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.FileVisitResult;
import java.nio.file.StandardOpenOption;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.OpenOption;
import java.nio.channels.FileChannel;
import java.nio.channels.OverlappingFileLockException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

/**
 * FileUtilは、共通的に使用されるファイル操作関連のメソッドを集約した、ユーティリティークラスです。
 *
 *<pre>
 * 読み込みチェックや、書き出しチェックなどの簡易的な処理をまとめているだけです。
 *
 *</pre>
 * @og.rev 7.0.0.0 (2017/07/07) 新規作成
 *
 * @version  7.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK1.8,
 */
public final class FileUtil {
	/** ファイルが安定するまでの待ち時間(ミリ秒) {@value} */
	public static final int STABLE_SLEEP_TIME  = 2000 ;	// ファイルが安定するまで、２秒待つ
	/** ファイルが安定するまでのリトライ回数 {@value} */
	public static final int STABLE_RETRY_COUNT = 10 ;	// ファイルが安定するまで、１０回リトライする。

	/** ファイルロックの獲得までの待ち時間(ミリ秒) {@value} */
	public static final int LOCK_SLEEP_TIME  = 2000 ;	// ロックの獲得まで、２秒待つ
	/** ファイルロックの獲得までのリトライ回数 {@value} */
	public static final int LOCK_RETRY_COUNT = 10 ;		// ロックの獲得まで、１０回リトライする。

	/** 日本語用の、Windows-31J の、Charset  */
	public static final Charset WINDOWS_31J = Charset.forName( "Windows-31J" );

	/** 日本語用の、UTF-8 の、Charset (Windows-31Jと同じように指定できるようにしておきます。)  */
	public static final Charset UTF_8		= StandardCharsets.UTF_8;

	private static final OpenOption[] CREATE = new OpenOption[] { StandardOpenOption.WRITE , StandardOpenOption.CREATE , StandardOpenOption.TRUNCATE_EXISTING };
	private static final OpenOption[] APPEND = new OpenOption[] { StandardOpenOption.WRITE , StandardOpenOption.CREATE , StandardOpenOption.APPEND };

	private static final Object STATIC_LOCK = new Object();		// staticレベルのロック

	/**
	 * デフォルトコンストラクターをprivateにして、
	 * オブジェクトの生成をさせないようにする。
	 */
	private FileUtil() {}

	/**
	 * 引数の文字列を連結した読み込み用パスのチェックを行い、存在する場合は、そのパスオブジェクトを返します。
	 *
	 * Paths#get(String,String...) で作成したパスオブジェクトに存在チェックを加えたものです。
	 * そのパスが存在しなければ、例外をThrowします。
	 *
	 * @og.rev 1.0.0 (2016/04/28) 新規追加
	 *
	 * @param	first	パス文字列またはパス文字列の最初の部分
	 * @param	more	結合してパス文字列を形成するための追加文字列
	 * @return	指定の文字列を連結したパスオブジェクト
	 * @throws	RuntimeException ファイル/フォルダは存在しない場合
	 * @see		Paths#get(String,String...)
	 */
	public static Path readPath( final String first , final String... more ) {
		final Path path = Paths.get( first,more ).toAbsolutePath().normalize() ;

		if( !Files.exists( path ) ) {
			// MSG0002 = ファイル/フォルダは存在しません。file=[{0}]
			throw MsgUtil.throwException( "MSG0002" , path );
		}

		return path;
	}

	/**
	 * 引数の文字列を連結した書き込み用パスを作成します。
	 *
	 * Paths#get(String,String...) で作成したパスオブジェクトに存在チェックを加え、
	 * そのパスが存在しなければ、作成します。
	 * パスが、フォルダの場合は、そのまま作成し、ファイルの場合は、親フォルダまでを作成します。
	 * パスがフォルダかファイルかの区別は、拡張子があるかどうかで判定します。
	 *
	 * @og.rev 1.0.0 (2016/04/28) 新規追加
	 *
	 * @param	first	パス文字列またはパス文字列の最初の部分
	 * @param	more	結合してパス文字列を形成するための追加文字列
	 * @return	指定の文字列を連結したパスオブジェクト
	 * @throws	RuntimeException ファイル/フォルダが作成できなかった場合
	 * @see		Paths#get(String,String...)
	 */
	public static Path writePath( final String first , final String... more ) {
		final Path path = Paths.get( first,more ).toAbsolutePath().normalize() ;

		mkdirs( path );

		return path;
	}

	/**
	 * ファイルオブジェクトを作成します。
	 *
	 * 通常は、フォルダ＋ファイル名で、新しいファイルオブジェクトを作成します。
	 * ここでは、第２引数のファイル名に、絶対パスを指定した場合は、第１引数の
	 * フォルダを使用せず、ファイル名だけで、ファイルオブジェクトを作成します。
	 * 第２引数のファイル名が、null か、ゼロ文字列の場合は、第１引数の
	 * フォルダを返します。
	 *
	 * @og.rev 7.2.1.0 (2020/03/13) isAbsolute(String)を利用します。
	 *
	 * @param path	基準となるフォルダ(ファイルの場合は、親フォルダ基準)
	 * @param fname	ファイル名(絶対パス、または、相対パス)
	 * @return 合成されたファイルオブジェクト
	 */
	public static Path newPath( final Path path , final String fname ) {
		if( fname == null || fname.isEmpty() ) {
			return path;
		}
//		else if( fname.charAt(0) == '/'  						||		// 実フォルダが UNIX
//				 fname.charAt(0) == '\\'						||		// 実フォルダが ネットワークパス
//				 fname.length() > 1 && fname.charAt(1) == ':' ) {		// 実フォルダが Windows
		else if( isAbsolute( fname ) ) {
			return new File( fname ).toPath();
		}
		else {
			return path.resolve( fname );
		}
	}

	/**
	 * ファイルアドレスが絶対パスかどうか[絶対パス:true]を判定します。
	 *
	 * ファイル名が、絶対パス('/' か、'\\' か、2文字目が ':' の場合)かどうかを
	 * 判定して、絶対パスの場合は、true を返します。
	 * それ以外(nullやゼロ文字列も含む)は、false になります。
	 *
	 * @og.rev 7.2.1.0 (2020/03/13) 新規追加
	 *
	 * @param fname	ファイルパスの文字列(絶対パス、相対パス、null、ゼロ文字列)
	 * @return 絶対パスの場合は true
	 */
	public static boolean isAbsolute( final String fname ) {
		return fname != null && (
				   fname.charAt(0) == '/' 								// 実フォルダが UNIX
				|| fname.charAt(0) == '\\'								// 実フォルダが ネットワークパス
				|| ( fname.length() > 1 && fname.charAt(1) == ':' ));	// 実フォルダが Windows
	}

	/**
	 * 引数のファイルパスを親階層を含めて生成します。
	 *
	 * すでに存在している場合や作成が成功した場合は、true を返します。
	 * 作成に失敗した場合は、false です。
	 * 指定のファイルパスは、フォルダであることが前提ですが、簡易的に
	 * ファイルの場合は、その親階層のフォルダを作成します。
	 * ファイルかフォルダの判定は、拡張子があるか、ないかで判定します。
	 *
	 * @og.rev 1.0.0 (2016/04/28) 新規追加
	 *
	 * @param	target	ターゲットのファイルパス
	 * @throws	RuntimeException フォルダの作成に失敗した場合
	 */
	public static void mkdirs( final Path target ) {
		if( Files.notExists( target ) ) {		// 存在しない場合
			// 6.9.8.0 (2018/05/28) FindBugs:null になっている可能性があるメソッドの戻り値を利用している
//			final boolean isFile = target.getFileName().toString().contains( "." );		// ファイルかどうかは、拡張子の有無で判定する。

			final Path tgtName = target.getFileName();
			if( tgtName == null ) {
				// MSG0007 = ファイル/フォルダの作成に失敗しました。\n\tdir=[{0}]
				throw MsgUtil.throwException( "MSG0007" , target.toString() );
			}

			final boolean isFile = tgtName.toString().contains( "." );					// ファイルかどうかは、拡張子の有無で判定する。
//			final Path dir = isFile ? target.toAbsolutePath().getParent() : target ;	// ファイルなら、親フォルダを取り出す。
			final Path dir = isFile ? target.getParent() : target ;						// ファイルなら、親フォルダを取り出す。
			if( dir == null ) {
				// MSG0007 = ファイル/フォルダの作成に失敗しました。\n\tdir=[{0}]
				throw MsgUtil.throwException( "MSG0007" , target.toString() );
			}

			if( Files.notExists( dir ) ) {		// 存在しない場合
				try {
					Files.createDirectories( dir );
				}
				catch( final IOException ex ) {
					// MSG0007 = ファイル/フォルダの作成に失敗しました。dir=[{0}]
					throw MsgUtil.throwException( ex , "MSG0007" , dir );
				}
			}
		}
	}

	/**
	 * 単体ファイルをコピーします。
	 *
	 * コピー先がなければ、コピー先のフォルダ階層を作成します。
	 * コピー先がフォルダの場合は、コピー元と同じファイル名で、コピーします。
	 * コピー先のファイルがすでに存在する場合は、上書きされますので、
	 * 必要であれば、先にバックアップしておいて下さい。
	 *
	 * @og.rev 1.0.0 (2016/04/28) 新規追加
	 *
	 * @param from	コピー元となるファイル
	 * @param to	コピー先となるファイル
	 * @throws	RuntimeException ファイル操作に失敗した場合
	 * @see		#copy(Path,Path,boolean)
	 */
	public static void copy( final Path from , final Path to ) {
		copy( from,to,false );
	}

	/**
	 * パスの共有ロックを指定した、単体ファイルをコピーします。
	 *
	 * コピー先がなければ、コピー先のフォルダ階層を作成します。
	 * コピー先がフォルダの場合は、コピー元と同じファイル名で、コピーします。
	 * コピー先のファイルがすでに存在する場合は、上書きされますので、
	 * 必要であれば、先にバックアップしておいて下さい。
	 *
	 * ※ copy に関しては、コピー時間を最小化する意味で、synchronized しています。
	 *
	 * @og.rev 1.0.0 (2016/04/28) 新規追加
	 *
	 * @param from	コピー元となるファイル
	 * @param to	コピー先となるファイル
	 * @param useLock	パスを共有ロックするかどうか
	 * @throws	RuntimeException ファイル操作に失敗した場合
	 * @see		#copy(Path,Path)
	 */
	public static void copy( final Path from , final Path to , final boolean useLock ) {
		if( Files.exists( from ) ) {
			mkdirs( to );

			// 6.9.8.0 (2018/05/28) FindBugs:null になっている可能性があるメソッドの戻り値を利用している
//			final boolean isFile = to.getFileName().toString().contains( "." );			// ファイルかどうかは、拡張子の有無で判定する。

			final Path toName = to.getFileName();
			if( toName == null ) {
				// MSG0008 = ファイルが移動できませんでした。\n\tfrom=[{0}] to=[{1}]
				throw MsgUtil.throwException( "MSG0008" , from.toString() , to.toString() );
			}

			final boolean isFile = toName.toString().contains( "." );		// ファイルかどうかは、拡張子の有無で判定する。

			// コピー先がフォルダの場合は、コピー元と同じ名前のファイルにする。
			final Path save = isFile ? to : to.resolve( from.getFileName() );

			synchronized( STATIC_LOCK ) {
				if( useLock ) {
					lockPath( from , in -> localCopy( in , save ) );
				}
				else {
					localCopy( from , save );
				}
			}
		}
		else {
			// MSG0002 = ファイル/フォルダが存在しません。file=[{0}]
			MsgUtil.errPrintln( "MSG0002" , from );
		}
	}

	/**
	 * 単体ファイルをコピーします。
	 *
	 * これは、IOException の処理と、直前の存在チェックをまとめたメソッドです。
	 *
	 * @og.rev 1.0.0 (2016/04/28) 新規追加
	 *
	 * @param from	コピー元となるファイル
	 * @param to	コピー先となるファイル
	 */
	private static void localCopy( final Path from , final Path to ) {
		try {
			// 直前に存在チェックを行います。
			if( Files.exists( from ) ) {
				Files.copy( from , to , StandardCopyOption.REPLACE_EXISTING );
			}
		}
		catch( final IOException ex ) {
			// MSG0012 = ファイルがコピーできませんでした。from=[{0}] to=[{1}]
			MsgUtil.errPrintln( ex , "MSG0012" , from , to );
		}
	}

	/**
	 * 単体ファイルを移動します。
	 *
	 * 移動先がなければ、移動先のフォルダ階層を作成します。
	 * 移動先がフォルダの場合は、移動元と同じファイル名で、移動します。
	 * 移動先のファイルがすでに存在する場合は、上書きされますので、
	 * 必要であれば、先にバックアップしておいて下さい。
	 *
	 * @og.rev 1.0.0 (2016/04/28) 新規追加
	 *
	 * @param from	移動元となるファイル
	 * @param to	移動先となるファイル
	 * @throws	RuntimeException ファイル操作に失敗した場合
	 * @see		#move(Path,Path,boolean)
	 */
	public static void move( final Path from , final Path to ) {
		move( from,to,false );
	}

	/**
	 * パスの共有ロックを指定した、単体ファイルを移動します。
	 *
	 * 移動先がなければ、移動先のフォルダ階層を作成します。
	 * 移動先がフォルダの場合は、移動元と同じファイル名で、移動します。
	 * 移動先のファイルがすでに存在する場合は、上書きされますので、
	 * 必要であれば、先にバックアップしておいて下さい。
	 *
	 * ※ move に関しては、ムーブ時間を最小化する意味で、synchronized しています。
	 *
	 * @og.rev 1.0.0 (2016/04/28) 新規追加
	 * @og.rev 7.2.1.0 (2020/03/13) from,to が null の場合、処理しない。
	 *
	 * @param from	移動元となるファイル
	 * @param to	移動先となるファイル
	 * @param useLock	パスを共有ロックするかどうか
	 * @throws	RuntimeException ファイル操作に失敗した場合
	 * @see		#move(Path,Path)
	 */
	public static void move( final Path from , final Path to , final boolean useLock ) {
		if( from == null || to == null ) { return; }			// 7.2.1.0 (2020/03/13)

		if( Files.exists( from ) ) {
			mkdirs( to );

			// ファイルかどうかは、拡張子の有無で判定する。
			// 6.9.8.0 (2018/05/28) FindBugs:null になっている可能性があるメソッドの戻り値を利用している
//			final boolean isFile = to.getFileName().toString().contains( "." );
			final Path toName = to.getFileName();
			if( toName == null ) {
				// MSG0008 = ファイルが移動できませんでした。\n\tfrom=[{0}] to=[{1}]
				throw MsgUtil.throwException( "MSG0008" , to.toString() );
			}

			final boolean isFile = toName.toString().contains( "." );		// ファイルかどうかは、拡張子の有無で判定する。

			// 移動先がフォルダの場合は、コピー元と同じ名前のファイルにする。
			final Path save = isFile ? to : to.resolve( from.getFileName() );

			synchronized( STATIC_LOCK ) {
				if( useLock ) {
					lockPath( from , in -> localMove( in , save ) );
				}
				else {
					localMove( from , save );
				}
			}
		}
		else {
			// MSG0002 = ファイル/フォルダが存在しません。file=[{0}]
			MsgUtil.errPrintln( "MSG0002" , from );
		}
	}

	/**
	 * 単体ファイルを移動します。
	 *
	 * これは、IOException の処理と、直前の存在チェックをまとめたメソッドです。
	 *
	 * @og.rev 1.0.0 (2016/04/28) 新規追加
	 *
	 * @param from	移動元となるファイル
	 * @param to	移動先となるファイル
	 */
	private static void localMove( final Path from , final Path to ) {
		try {
			// 直前に存在チェックを行います。
			if( Files.exists( from ) ) {
				// CopyOption に、StandardCopyOption.ATOMIC_MOVE を指定すると、別サーバー等へのMOVEは、出来なくなります。
				Files.move( from , to , StandardCopyOption.REPLACE_EXISTING );
			}
		}
		catch( final IOException ex ) {
			// MSG0008 = ファイルが移動できませんでした。from=[{0}] to=[{1}]
			MsgUtil.errPrintln( ex , "MSG0008" , from , to );
		}
	}

	/**
	 * 単体ファイルをバックアップフォルダに移動します。
	 *
	 * これは、#backup( from,to,true,false,sufix ); と同じ処理を実行します。
	 *
	 * 移動先は、フォルダ指定で、ファイル名は存在チェックせずに、必ず変更します。
	 * その際、移動元＋サフィックス のファイルを作成します。
	 * ファイルのロックを行います。
	 *
	 * @og.rev 1.0.0 (2016/04/28) 新規追加
	 *
	 * @param from	移動元となるファイル
	 * @param to	移動先となるフォルダ(nullの場合は、移動元と同じフォルダ)
	 * @param sufix	バックアップファイル名の後ろに付ける文字列
	 * @return	バックアップしたファイルパス。
	 * @throws	RuntimeException ファイル操作に失敗した場合
	 * @see	#backup( Path , Path , boolean , boolean , String )
	 */
	public static Path backup( final Path from , final Path to , final String sufix ) {
		return backup( from,to,true,false,sufix );
	}

	/**
	 * 単体ファイルをバックアップフォルダに移動します。
	 *
	 * これは、#backup( from,to,true,true ); と同じ処理を実行します。
	 *
	 * 移動先は、フォルダ指定で、ファイル名は存在チェックの上で、無ければ移動、
	 * あれば、移動元＋時間情報 のファイルを作成します。
	 * ファイルのロックを行います。
	 * 移動先を指定しない(=null)場合は、自分自身のフォルダでの、ファイル名変更になります。
	 *
	 * @og.rev 1.0.0 (2016/04/28) 新規追加
	 *
	 * @param from	移動元となるファイル
	 * @param to	移動先となるフォルダ(nullの場合は、移動元と同じフォルダ)
	 * @return	バックアップしたファイルパス。
	 * @throws	RuntimeException ファイル操作に失敗した場合
	 * @see	#backup( Path , Path , boolean , boolean , String )
	 */
	public static Path backup( final Path from , final Path to ) {
		return backup( from,to,true,true,null );
	}

	/**
	 * パスの共有ロックを指定して、単体ファイルをバックアップフォルダに移動します。
	 *
	 * 移動先のファイル名は、existsCheckが、trueの場合は、移動先のファイル名をチェックして、
	 * 存在しなければ、移動元と同じファイル名で、バックアップフォルダに移動します。
	 * 存在すれば、ファイル名＋サフィックス のファイルを作成します。(拡張子より後ろにサフィックスを追加します。)
	 * existsCheckが、false の場合は、無条件に、移動元のファイル名に、サフィックスを追加します。
	 * サフィックスがnullの場合は、時間情報になります。
	 * 移動先を指定しない(=null)場合は、自分自身のフォルダでの、ファイル名変更になります。
	 *
	 * @og.rev 1.0.0 (2016/04/28) 新規追加
	 * @og.rev 6.9.8.0 (2018/05/28) FindBugs:null になっている可能性があるメソッドの戻り値を利用している
	 * @og.rev 7.2.1.0 (2020/03/13) ﾌｧｲﾙ名変更処理の修正
	 *
	 * @param from	移動元となるファイル
	 * @param to	移動先となるフォルダ(nullの場合は、移動元と同じフォルダ)
	 * @param useLock	パスを共有ロックするかどうか
	 * @param existsCheck	移動先のファイル存在チェックを行うかどうか(true:行う/false:行わない)
	 * @param sufix	バックアップファイル名の後ろに付ける文字列
	 *
	 * @return	バックアップしたファイルパス。
	 * @throws	RuntimeException ファイル操作に失敗した場合
	 * @see	#backup( Path , Path )
	 */
	public static Path backup( final Path from , final Path to , final boolean useLock , final boolean existsCheck , final String sufix ) {
		final Path movePath = to == null ? from.getParent() : to ;

		// 6.9.8.0 (2018/05/28) FindBugs:null になっている可能性があるメソッドの戻り値を利用している
		if( movePath == null ) {
			// MSG0007 = ファイル/フォルダの作成に失敗しました。\n\tdir=[{0}]
			throw MsgUtil.throwException( "MSG0007" , from.toString() );
		}

//		final String fileName = from.getFileName().toString();
		final Path	fName = from.getFileName();
		if( fName == null ) {
			// MSG0002 = ファイル/フォルダが存在しません。\n\tfile=[{0}]
			throw MsgUtil.throwException( "MSG0002" , from.toString() );
		}

//		final Path	moveFile = movePath.resolve( fileName );					// 移動先のファイルパスを構築
		final Path	moveFile = movePath.resolve( fName );						// 移動先のファイルパスを構築

//		final boolean isExChk = existsCheck && Files.notExists( moveFile );		// 存在しない場合、true。存在するか、不明の場合は、false。

		final Path bkupPath;
//		if( isExChk ) {
		if( existsCheck && Files.notExists( moveFile ) ) {				// 存在しない場合、true。存在するか、不明の場合は、false。
			bkupPath = moveFile;
		}
		else {
			final String fileName = fName.toString();					// from パスの名前
			final int ad = fileName.lastIndexOf( '.' );					// ピリオドの手前に、タイムスタンプを入れる。
			// 7.2.1.0 (2020/03/13) ﾌｧｲﾙ名変更処理の修正
			if( ad > 0 ) {
				bkupPath = movePath.resolve(
								fileName.substring( 0,ad )
								+ "_"
								+ StringUtil.nval( sufix , StringUtil.getTimeFormat() )
								+ fileName.substring( ad )				// ad 以降なので、ピリオドも含む
						);
			}
			else {
				bkupPath = null;
			}
		}

		move( from,bkupPath,useLock );

		return bkupPath;
	}

	/**
	 * ファイルまたはフォルダ階層を削除します。
	 *
	 * これは、指定のパスが、フォルダの場合、階層すべてを削除します。
	 * 階層の途中にファイル等が存在していたとしても、削除します。
	 * 
	 * Files.walkFileTree(Path,FileVisitor) を使用したファイル・ツリーの削除方式です。
	 *
	 * @og.rev 1.0.0 (2016/04/28) 新規追加
	 *
	 * @param start	削除開始ファイル
	 * @throws	RuntimeException ファイル操作に失敗した場合
	 */
	public static void delete( final Path start ) {
		try {
			if( Files.exists( start ) ) {
				Files.walkFileTree( start, DELETE_VISITOR );
			}
		}
		catch( final IOException ex ) {
			// MSG0011 = ファイルが削除できませんでした。file=[{0}]
			throw MsgUtil.throwException( ex , "MSG0011" , start );
		}
	}

	/**
	 * delete(Path)で使用する、Files.walkFileTree の引数の FileVisitor オブジェクトです。
	 *
	 * staticオブジェクトを作成しておき、使いまわします。
	 */
	private static final FileVisitor<Path> DELETE_VISITOR = new SimpleFileVisitor<Path>() {
		/**
		 * ディレクトリ内のファイルに対して呼び出されます。
		 *
		 * @param file	ファイルへの参照
		 * @param attrs	ファイルの基本属性
		 * @throws	IOException 入出力エラーが発生した場合
		 */
		@Override
		public FileVisitResult visitFile( final Path file, final BasicFileAttributes attrs ) throws IOException {
			Files.deleteIfExists( file );		// ファイルが存在する場合は削除
			return FileVisitResult.CONTINUE;
		}

		/**
		 * ディレクトリ内のエントリ、およびそのすべての子孫がビジットされたあとにそのディレクトリに対して呼び出されます。
		 *
		 * @param dir	ディレクトリへの参照
		 * @param ex	エラーが発生せずにディレクトリの反復が完了した場合はnull、そうでない場合はディレクトリの反復が早く完了させた入出力例外
		 * @throws	IOException 入出力エラーが発生した場合
		 */
		@Override
		public FileVisitResult postVisitDirectory( final Path dir, final IOException ex ) throws IOException {
			if( ex == null ) {
				Files.deleteIfExists( dir );		// ファイルが存在する場合は削除
				return FileVisitResult.CONTINUE;
			} else {
				// directory iteration failed
				throw ex;
			}
		}
	};

	/**
	 * 指定のパスのファイルが、書き込まれている途中かどうかを判定し、落ち着くまで待ちます。
	 *
	 * FileUtil.stablePath( path , STABLE_SLEEP_TIME , STABLE_RETRY_COUNT ); と同じです。
	 *
	 * @param	path  チェックするパスオブジェクト
	 * @return	true:安定した/false:安定しなかった。またはファイルが存在していない。
	 * @see		#STABLE_SLEEP_TIME
	 * @see		#STABLE_RETRY_COUNT
	 */
	public static boolean stablePath( final Path path ) {
		return stablePath( path , STABLE_SLEEP_TIME , STABLE_RETRY_COUNT );
	}

	/**
	 * 指定のパスのファイルが、書き込まれている途中かどうかを判定し、落ち着くまで待ちます。
	 *
	 * ファイルの安定は、ファイルのサイズをチェックすることで求めます。まず、サイズをチェックし、
	 * sleepで指定した時間だけ、Thread.sleepします。再び、サイズをチェックして、同じであれば、
	 * 安定したとみなします。
	 * なので、必ず、sleep で指定したミリ秒だけは、待ちます。
	 * ファイルが存在しない、サイズが、０のままか、チェック回数を過ぎても安定しない場合は、
	 * false が返ります。
	 * サイズを求める際に、IOExceptionが発生した場合でも、falseを返します。
	 *
	 * @param	path  チェックするパスオブジェクト
	 * @param	sleep 待機する時間(ミリ秒)
	 * @param	cnt   チェックする回数
	 * @return	true:安定した/false:安定しなかった。またはファイルが存在していない。
	 */
	public static boolean stablePath( final Path path , final long sleep , final int cnt ) {
		// 存在しない場合は、即抜けます。
		if( Files.exists( path ) ) {
			try {
				for( int i=0; i<cnt; i++ ) {
					if( Files.notExists( path ) ) { return false; }								// 存在チェック。無ければ、false
					final long size1 = Files.size( path );										// exit point 警告が出ますが、Thread.sleep 前に、値を取得しておきたい。

					try{ Thread.sleep( sleep ); } catch( final InterruptedException ex ){}		// 無条件に待ちます。

					if( Files.notExists( path ) ) { return false; }								// 存在チェック。無ければ、false
					final long size2 = Files.size( path );
					if( size1 != 0L && size1 == size2 ) { return true; }						// 安定した
				}
			}
			catch( final IOException ex ) {
				// Exception は発生させません。
				// MSG0005 = フォルダのファイル読み込み時にエラーが発生しました。file=[{0}] 
				MsgUtil.errPrintln( ex , "MSG0005" , path );
			}
		}

		return false;
	}

	/**
	 * 指定のパスを共有ロックして、Consumer#action(Path) メソッドを実行します。
	 * 共有ロック中は、ファイルを読み込むことは出来ますが、書き込むことは出来なくなります。
	 *
	 * 共有ロックの取得は、{@value #LOCK_RETRY_COUNT} 回実行し、{@value #LOCK_SLEEP_TIME} ミリ秒待機します。
	 *
	 * @param inPath	処理対象のPathオブジェクト
	 * @param action	パスを引数に取るConsumerオブジェクト
	 * @throws	RuntimeException ファイル読み込み時にエラーが発生した場合
	 * @see		#forEach(Path,Consumer)
	 * @see		#LOCK_RETRY_COUNT
	 * @see		#LOCK_SLEEP_TIME
	 */
	public static void lockPath( final Path inPath , final Consumer<Path> action ) {
		// 処理の直前で、処理対象のファイルが存在しているかどうか確認します。
		if( Files.exists( inPath ) ) {
			// try-with-resources 文 (AutoCloseable)
			try( final FileChannel channel = FileChannel.open( inPath, StandardOpenOption.READ ) ) {
				 for( int i=0; i<LOCK_RETRY_COUNT; i++ ) {
					try {
						if( channel.tryLock( 0L,Long.MAX_VALUE,true ) != null ) {	// 共有ロック獲得成功
							action.accept( inPath );
							return;		// 共有ロック獲得成功したので、ループから抜ける。
						}
					}
					catch( final OverlappingFileLockException ex ) {
						// 要求された領域をオーバーラップするロックがこのJava仮想マシンにすでに確保されている場合。
						// または、このメソッド内でブロックされている別のスレッドが同じファイルのオーバーラップした領域をロックしようとしている場合
						System.err.println( ex.getMessage() );
					}
					try{ Thread.sleep( LOCK_SLEEP_TIME ); } catch( final InterruptedException ex ){}
				}
			}
			catch( final IOException ex ) {
				// MSG0005 = フォルダのファイル読み込み時にエラーが発生しました。file=[{0}]
				throw MsgUtil.throwException( ex , "MSG0005" , inPath );
			}

			// Exception は発生させません。
			// MSG0015 = ファイルのロック取得に失敗しました。file=[{0}] WAIT=[{1}](ms) COUNT=[{2}]
			MsgUtil.errPrintln( "MSG0015" , inPath , LOCK_SLEEP_TIME , LOCK_RETRY_COUNT );
		}
	}

	/**
	 * 指定のパスから、１行づつ読み取った結果をConsumerにセットする繰り返しメソッドです。
	 * １行単位に、Consumer#action が呼ばれます。
	 * このメソッドでは、Charset は、UTF-8 です。
	 *
	 * ファイルを順次読み込むため、内部メモリを圧迫しません。
	 *
	 * @param inPath	処理対象のPathオブジェクト
	 * @param action	行を引数に取るConsumerオブジェクト
	 * @throws	RuntimeException ファイル読み込み時にエラーが発生した場合
	 * @see		#lockForEach(Path,Consumer)
	 */
	public static void forEach( final Path inPath , final Consumer<String> action ) {
		forEach( inPath , UTF_8 , action );
	}

	/**
	 * 指定のパスから、１行づつ読み取った結果をConsumerにセットする繰り返しメソッドです。
	 * １行単位に、Consumer#action が呼ばれます。
	 *
	 * ファイルを順次読み込むため、内部メモリを圧迫しません。
	 *
	 * @param inPath	処理対象のPathオブジェクト
	 * @param chset		ファイルを読み取るときのCharset
	 * @param action	行を引数に取るConsumerオブジェクト
	 * @throws	RuntimeException ファイル読み込み時にエラーが発生した場合
	 * @see		#lockForEach(Path,Consumer)
	 */
	public static void forEach( final Path inPath , final Charset chset , final Consumer<String> action ) {
		// 処理の直前で、処理対象のファイルが存在しているかどうか確認します。
		if( Files.exists( inPath ) ) {
			// try-with-resources 文 (AutoCloseable)
			String line = null;
			int no = 0;
	//		// こちらの方法では、lockForEach から来た場合に、エラーになります。
	//		try( final BufferedReader reader = Files.newBufferedReader( inPath , chset ) ) {
			// 万一、コンストラクタでエラーが発生すると、リソース開放されない場合があるため、個別にインスタンスを作成しておきます。(念のため)
			try( final FileInputStream   fin = new FileInputStream( inPath.toFile() );
				 final InputStreamReader isr = new InputStreamReader( fin , chset );
				 final BufferedReader reader = new BufferedReader( isr ) ) {
				while( ( line = reader.readLine() ) != null ) {
					action.accept( line );
					no++;
				}
			}
			catch( final IOException ex ) {
				// MSG0016 = ファイルの行データ読み込みに失敗しました。file:行番号:行\n\t{0}:{1}: {2}
				throw MsgUtil.throwException( ex , "MSG0016" , inPath , no , line );
			}
		}
	}

	/**
	 * 指定のパスを共有ロックして、１行づつ読み取った結果をConsumerにセットする繰り返しメソッドです。
	 * １行単位に、Consumer#action が呼ばれます。
	 *
	 * ファイルを順次読み込むため、内部メモリを圧迫しません。
	 *
	 * @param inPath	処理対象のPathオブジェクト
	 * @param action	行を引数に取るConsumerオブジェクト
	 * @see		#forEach(Path,Consumer)
	 */
	public static void lockForEach( final Path inPath , final Consumer<String> action ) {
		lockPath( inPath , in -> forEach( in , UTF_8 , action ) );
	}

	/**
	 * 指定のパスを共有ロックして、１行づつ読み取った結果をConsumerにセットする繰り返しメソッドです。
	 * １行単位に、Consumer#action が呼ばれます。
	 *
	 * ファイルを順次読み込むため、内部メモリを圧迫しません。
	 *
	 * @param inPath	処理対象のPathオブジェクト
	 * @param chset		エンコードを指定するCharsetオブジェクト
	 * @param action	行を引数に取るConsumerオブジェクト
	 * @see		#forEach(Path,Consumer)
	 */
	public static void lockForEach( final Path inPath , final Charset chset , final Consumer<String> action ) {
		lockPath( inPath , in -> forEach( in , chset , action ) );
	}

	/**
	 * 指定のパスに１行単位の文字列のListを書き込んでいきます。
	 * １行単位の文字列のListを作成しますので、大きなファイルの作成には向いていません。
	 *
	 * 書き込むパスの親フォルダがなければ作成します。
	 * 第２引数は、書き込む行データです。
	 * このメソッドでは、Charset は、UTF-8 です。
	 *
	 * @og.rev 1.0.0 (2016/04/28) 新規追加
	 *
	 * @param	savePath セーブするパスオブジェクト
	 * @param	lines	行単位の書き込むデータ
	 * @throws	RuntimeException ファイル操作に失敗した場合
	 * @see		#save( Path , List , boolean , Charset )
	 */
	public static void save( final Path savePath , final List<String> lines ) {
		save( savePath , lines , false , UTF_8 );		// 新規作成
	}

	/**
	 * 指定のパスに１行単位の文字列のListを書き込んでいきます。
	 * １行単位の文字列のListを作成しますので、大きなファイルの作成には向いていません。
	 *
	 * 書き込むパスの親フォルダがなければ作成します。
	 *
	 * 第２引数は、書き込む行データです。
	 *
	 * @og.rev 1.0.0 (2016/04/28) 新規追加
	 *
	 * @param	savePath セーブするパスオブジェクト
	 * @param	lines	行単位の書き込むデータ
	 * @param	append	trueの場合、ファイルの先頭ではなく最後に書き込まれる。
	 * @param	chset	ファイルを読み取るときのCharset
	 * @throws	RuntimeException ファイル操作に失敗した場合
	 */
	public static void save( final Path savePath , final List<String> lines , final boolean append , final Charset chset ) {
		// 6.9.8.0 (2018/05/28) FindBugs:null になっている可能性があるメソッドの戻り値を利用している
		// ※ toAbsolutePath() する必要はないのと、getParent() は、null を返すことがある
//		mkdirs( savePath.toAbsolutePath().getParent() );		// savePathはファイルなので、親フォルダを作成する。
		final Path parent = savePath.getParent();
		if( parent == null ) {
			// MSG0007 = ファイル/フォルダの作成に失敗しました。\n\tdir=[{0}]
			throw MsgUtil.throwException( "MSG0007" , savePath.toString() );
		}
		else {
			mkdirs( parent );
		}

		String line = null;		// エラー出力のための変数
		int no = 0;

		// try-with-resources 文 (AutoCloseable)
		try( final PrintWriter out = new PrintWriter( Files.newBufferedWriter( savePath, chset , append ? APPEND : CREATE ) ) ) {
			 for( final String ln : lines ) {
				line = ln ;
				no++;
				out.println( line );
			}
			out.flush();
		}
		catch( final IOException ex ) {
			// MSG0017=ファイルのデータ書き込みに失敗しました。file:行番号:行\n\t{0}:{1}: {2}
			throw MsgUtil.throwException( ex , "MSG0017" , savePath , no , line );
		}
	}

	/**
	 * 指定のパスの最終更新日付を、文字列で返します。
	 * 文字列のフォーマット指定も可能です。
	 *
	 * パスが無い場合や、最終更新日付を、取得できない場合は、現在時刻をベースに返します。
	 *
	 * @param path		処理対象のPathオブジェクト
	 * @param format	文字列化する場合のフォーマット(yyyyMMddHHmmss)
	 * @return	指定のパスの最終更新日付の文字列
	 */
	public static String timeStamp( final Path path , final String format ) {
		long tempTime = 0L;
		try {
			// 存在チェックを直前に入れますが、厳密には、非同期なので確率の問題です。
			if( Files.exists( path ) ) {
				tempTime = Files.getLastModifiedTime( path ).toMillis();
			}
		}
		catch( final IOException ex ) {
			// ファイルのタイムスタンプの取得に失敗しました。file=[{0}]
			MsgUtil.errPrintln( ex , "MSG0018" , path , ex.getMessage() );
		}
		if( tempTime == 0L ) {
			tempTime = System.currentTimeMillis();		// パスが無い場合や、エラー時は、現在時刻を使用
		}

		return StringUtil.getTimeFormat( tempTime , format );
	}

//	/** main メソッドから呼ばれる ヘルプメッセージです。 {@value}  */
//	public static final String USAGE = "Usage: java jp.euromap.eu63.util.FileUtil [-MOVE|-COPY|-DELETE|-BACKUP|-SAVE] from to [-useLock] [-append] [-help]" ;
//
//	/**
//	 * リソース一覧を表示する main メソッドです。
//	 *
//	 * @param	args	コマンド引数配列
//	 */
//	public static void main( final String[] args ) {
//		// ********** 【整合性チェック】 **********
//		if( args.length < 1 ) {
//			System.out.println( USAGE );
//			return;
//		}
//
//		// ********** 【引数定義】 **********
//		Path		fromPath	= null;					// 入力ファイル
//		Path		toPath		= null;					// 出力ファイル
//		boolean		isLock		= false;				// ロック処理(初期値:false しない)
//		boolean		isAppend	= false;				// 追記処理(初期値:false しない)
//		int			type		= -1;					// 0:MOVE , 1:COPY , 2:DELETE
//
//		// ********** 【引数処理】 **********
//		int cnt = 0 ;
//		for( final String arg : args ) {
//			if(      "-help"     .equalsIgnoreCase( arg ) ) { System.out.println( USAGE ); return ; }
//			else if( "-useLock"  .equalsIgnoreCase( arg ) ) { isLock	= true; }
//			else if( "-append "  .equalsIgnoreCase( arg ) ) { isAppend	= true; }
//			else if( "-MOVE"     .equalsIgnoreCase( arg ) ) { type		= 0; }
//			else if( "-COPY"     .equalsIgnoreCase( arg ) ) { type		= 1; }
//			else if( "-DELETE"   .equalsIgnoreCase( arg ) ) { type		= 2; }
//			else if( "-BACKUP"   .equalsIgnoreCase( arg ) ) { type		= 3; }
//			else if( "-SAVE"     .equalsIgnoreCase( arg ) ) { type		= 4; }
//			else { 
//				if(      cnt == 0 ) { fromPath = FileUtil.readPath(  arg ); }
//				else if( cnt == 1 ) { toPath   = FileUtil.writePath( arg ); }			// 親フォルダがなければ作成されます。
//				cnt++ ;
//			}
//		}
//
//		// ********** 【本体処理】 **********
//		switch( type ) {
//			case 0: 	System.out.println( "TYPE=MOVE FROM=" + fromPath + " , TO=" + toPath );
//						FileUtil.move( fromPath ,  toPath , isLock );
//						break;
//			case 1: 	System.out.println( "TYPE=COPY FROM=" + fromPath + " , TO=" + toPath );
//						FileUtil.copy( fromPath ,  toPath , isLock );
//						break;
//			case 2: 	System.out.println( "TYPE=DELETE START=" + fromPath );
//						FileUtil.delete( fromPath );
//						break;
//			case 3: 	System.out.println( "TYPE=BACKUP FROM=" + fromPath + " , TO=" + toPath );
//						FileUtil.backup( fromPath ,  toPath , isLock , false , null );
//						break;
//			case 4: 	System.out.println( "TYPE=SAVE FROM=" + fromPath + " , TO=" + toPath );
//						if( isLock ) {
//							final List<String> lines = new java.util.ArrayList<>();
//							FileUtil.lockForEach( fromPath , str -> lines.add( str ) );
//						}
//						else {
//							final List<String> lines = new java.util.ArrayList<>();
//							FileUtil.forEach( fromPath , str -> lines.add( str ) );
//							FileUtil.save( toPath , lines , isAppend , FileUtil.UTF_8 );
//						}
//						break;
//			default :	System.out.println( USAGE );
//						break;
//		}
//	}
}
