/*
 * 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.file.NoSuchFileException;					// 7.2.5.0 (2020/06/01)
import java.nio.file.AccessDeniedException;					// 8.0.0.0 (2021/07/31)
import java.nio.channels.FileChannel;
import java.nio.channels.OverlappingFileLockException;
import java.nio.charset.Charset;
import java.nio.charset.MalformedInputException;			// 7.2.5.0 (2020/06/01)
import static java.nio.charset.StandardCharsets.UTF_8;		// 7.2.5.0 (2020/06/01)

/**
 * 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 {
	private static final XLogger LOGGER= XLogger.getLogger( FileUtil.class.getSimpleName() );		// ログ出力

	/** ファイルが安定するまでの待ち時間(ミリ秒) {@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) 新規追加
	 * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。
	 *
	 * @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 ) ) {
		if( !exists( path ) ) {							// 7.2.5.0 (2020/06/01)
			// MSG0002 = ﾌｧｲﾙ/ﾌｫﾙﾀﾞは存在しません。file=[{0}]
//			throw MsgUtil.throwException( "MSG0002" , path );
			final String errMsg = "FileUtil#readPath : Path=" + path ;
			throw MsgUtil.throwException( "MSG0002" , errMsg );
		}

		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,false );

		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 && (
		return fname != null && !fname.isEmpty() && (
				   fname.charAt(0) == '/' 								// 実フォルダが UNIX
				|| fname.charAt(0) == '\\'								// 実フォルダが ネットワークパス
				|| fname.length() > 1 && fname.charAt(1) == ':' );		// 実フォルダが Windows
	}

	/**
	 * 引数のファイルパスを親階層を含めて生成します。
	 *
	 * すでに存在している場合や作成が成功した場合は、true を返します。
	 * 作成に失敗した場合は、false です。
	 * 指定のファイルパスは、フォルダであることが前提ですが、簡易的に
	 * ファイルの場合は、その親階層のフォルダを作成します。
	 * ファイルかフォルダの判定は、拡張子があるか、ないかで判定します。
	 *
	 * @og.rev 1.0.0 (2016/04/28) 新規追加
	 * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。
	 * @og.rev 8.0.0.0 (2021/07/01) STATIC_LOCKのsynchronized作成
	 *
	 * @param	target	ターゲットのファイルパス
	 * @param	parentCheck	先に親ﾌｫﾙﾀﾞの作成を行うかどうか(true:行う)
	 * @throws	RuntimeException フォルダの作成に失敗した場合
	 */
//	public static void mkdirs( final Path target ) {
	public static void mkdirs( final Path target,final boolean parentCheck ) {
//		if( Files.notExists( target ) ) {		// 存在しない場合
		if( !exists( target ) ) {				// 存在しない場合 7.2.5.0 (2020/06/01)
			// 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 ) ) {		// 存在しない場合
			if( !exists( dir ) ) {				// 存在しない場合 7.2.5.0 (2020/06/01)
				try {
					synchronized( STATIC_LOCK ) {			// 8.0.0.0 (2021/07/01) 意味があるかどうかは不明
						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) 新規追加
	 * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。
	 * @og.rev 7.3.1.3 (2021/03/09) 処理の直前にロックを掛けてから存在チェックを行います。
	 *
	 * @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 ) ) {
		if( exists( from ) ) {							// 7.2.5.0 (2020/06/01)
			mkdirs( to,false );

			// 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 ) {
				// 7.3.1.3 (2021/03/09) 処理の直前にロックを掛けてから存在チェックを行います。
				if( exists( from ) ) {
					if( useLock ) {
						lockPath( from , in -> localCopy( in , save ) );
					}
					else {
						localCopy( from , save );
					}
				}
			}
		}
		else {
			// 7.2.5.0 (2020/06/01)
			// MSG0002 = ﾌｧｲﾙ/ﾌｫﾙﾀﾞが存在しません。file=[{0}]
//			MsgUtil.errPrintln( "MSG0002" , from );
			final String errMsg = "FileUtil#copy : from=" + from ;
			LOGGER.warning( "MSG0002" , errMsg );
		}
	}

	/**
	 * 単体ファイルをコピーします。
	 *
	 * これは、IOException の処理と、直前の存在チェックをまとめたメソッドです。
	 *
	 * @og.rev 1.0.0 (2016/04/28) 新規追加
	 * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。
	 * @og.rev 7.3.1.3 (2021/03/09) 処理の直前にロックを掛けてから存在チェックを行います。
	 * @og.rev 7.4.4.0 (2021/06/30) copy/move がきちんとできたか確認します(ﾌｧｲﾙｻｲｽﾞﾁｪｯｸ)
	 *
	 * @param from	コピー元となるファイル
	 * @param to	コピー先となるファイル
	 */
	private static void localCopy( final Path from , final Path to ) {
		try {
			// 直前に存在チェックを行います。
//			if( Files.exists( from ) ) {
			// 7.3.1.3 (2021/03/09) 処理の直前にロックを掛けてから存在チェックを行います。
	//		synchronized( STATIC_LOCK ) {						// 7.4.4.0 (2021/06/30) 意味がないので外す。
				if( exists( from ) ) {							// 7.2.5.0 (2020/06/01)
					final long fromSize = Files.size(from);		// 7.4.4.0 (2021/06/30)
					Files.copy( from , to , StandardCopyOption.REPLACE_EXISTING );

					// 7.4.4.0 (2021/06/30) copy/move がきちんとできたか確認します(ﾌｧｲﾙｻｲｽﾞﾁｪｯｸ)
					for( int i=0; i<STABLE_RETRY_COUNT; i++ ) {
						// 8.0.0.0 (2021/07/31) Avoid if (x != y) ..; else ..;
						if( fromSize == Files.size(to) ) {
							return ;
						}
						else {
							try{ Thread.sleep( STABLE_SLEEP_TIME ); } catch( final InterruptedException ex ){}
						}

//						final long toSize = Files.size(to);
//						if( fromSize != toSize ) {
//							try{ Thread.sleep( STABLE_SLEEP_TIME ); } catch( final InterruptedException ex ){}
//						}
//						else {
//							break;
//						}
					}
				}
	//		}
		}
		catch( final NoSuchFileException ex ) {				// 8.0.0.0 (2021/07/31)
			// MSG0002 = ﾌｧｲﾙ/ﾌｫﾙﾀﾞが存在しません。\n\tfile=[{0}]
			LOGGER.warning( "MSG0002" , from );
			// MSG0012 = ﾌｧｲﾙがｺﾋﾟｰできませんでした。from=[{0}] to=[{1}]
			LOGGER.warning( "MSG0012" , from , to );		// 原因不明：FileWatchとDirWatchの両方が動いているから？
		}
		catch( final IOException ex ) {
			// MSG0012 = ﾌｧｲﾙがｺﾋﾟｰできませんでした。from=[{0}] to=[{1}]
//			MsgUtil.errPrintln( ex , "MSG0012" , from , to );
			LOGGER.warning( 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 の場合、処理しない。
	 * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。
	 * @og.rev 7.3.1.3 (2021/03/09) 処理の直前にロックを掛けてから存在チェックを行います。
	 *
	 * @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 ) ) {
		if( exists( from ) ) {					// 1.4.0 (2019/09/01)
			mkdirs( to,false );

			// ファイルかどうかは、拡張子の有無で判定する。
			// 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 ) {
				// 7.3.1.3 (2021/03/09) 処理の直前にロックを掛けてから存在チェックを行います。
				if( exists( from ) ) {
					if( useLock ) {
						lockPath( from , in -> localMove( in , save ) );
					}
					else {
						localMove( from , save );
					}
				}
			}
		}
		else {
			// MSG0002 = ﾌｧｲﾙ/ﾌｫﾙﾀﾞが存在しません。file=[{0}]
//			MsgUtil.errPrintln( "MSG0002" , from );
			final String errMsg = "FileUtil#move : from=" + from ;
			LOGGER.warning( "MSG0002" , errMsg );
		}
	}

	/**
	 * 単体ファイルを移動します。
	 *
	 * これは、IOException の処理と、直前の存在チェックをまとめたメソッドです。
	 *
	 * @og.rev 1.0.0 (2016/04/28) 新規追加
	 * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。
	 * @og.rev 7.3.1.3 (2021/03/09) 処理の直前にロックを掛けてから存在チェックを行います。
	 * @og.rev 7.4.4.0 (2021/06/30) copy/move がきちんとできたか確認します(ﾌｧｲﾙｻｲｽﾞﾁｪｯｸ)
	 *
	 * @param from	移動元となるファイル
	 * @param to	移動先となるファイル
	 */
	private static void localMove( final Path from , final Path to ) {
		try {
	//		synchronized( from ) {
				// 直前に存在チェックを行います。
//				if( Files.exists( from ) ) {
				// 7.3.1.3 (2021/03/09) 処理の直前にロックを掛けてから存在チェックを行います。
	//			synchronized( STATIC_LOCK ) {						// 7.4.4.0 (2021/06/30) 意味がないので外す。
					if( exists( from ) ) {							// このメソッドの結果がすぐに古くなることに注意してください。
						// CopyOption に、StandardCopyOption.ATOMIC_MOVE を指定すると、別サーバー等へのMOVEは、出来なくなります。
					//	try{ Thread.sleep( 2000 ); } catch( final InterruptedException ex ){}				// 先に、無条件に待ちます。
						final long fromSize = Files.size(from);		// 7.4.4.0 (2021/06/30)
						Files.move( from , to , StandardCopyOption.REPLACE_EXISTING );

						// 7.4.4.0 (2021/06/30) copy/move がきちんとできたか確認します(ﾌｧｲﾙｻｲｽﾞﾁｪｯｸ)
						for( int i=0; i<STABLE_RETRY_COUNT; i++ ) {
							// 8.0.0.0 (2021/07/31) Avoid if (x != y) ..; else ..;
							if( fromSize == Files.size(to) ) {
								return ;
							}
							else {
								try{ Thread.sleep( STABLE_SLEEP_TIME ); } catch( final InterruptedException ex ){}
							}

//							final long toSize = Files.size(to);
//							if( fromSize != toSize ) {
//								try{ Thread.sleep( STABLE_SLEEP_TIME ); } catch( final InterruptedException ex ){}
//							}
//							else {
//								break;
//							}
						}
					}
	//			}
	//		}
		}
		catch( final AccessDeniedException ex ) {				// 8.0.0.0 (2021/07/31)
			// MSG0034 = ﾌｧｲﾙｻｲｽﾞの取得ができませんでした。\n\tfile=[{0}]
			LOGGER.warning( "MSG0034" , from );
			// MSG0008 = ﾌｧｲﾙが移動できませんでした。from=[{0}] to=[{1}]
			LOGGER.warning( "MSG0008" , from , to );		// 原因不明：FileWatchとDirWatchの両方が動いているから？
		}
		catch( final NoSuchFileException ex ) {				// 7.2.5.0 (2020/06/01)
			// MSG0002 = ﾌｧｲﾙ/ﾌｫﾙﾀﾞが存在しません。\n\tfile=[{0}]
			LOGGER.warning( "MSG0002" , from );
			// MSG0008 = ﾌｧｲﾙが移動できませんでした。from=[{0}] to=[{1}]
			LOGGER.warning( "MSG0008" , from , to );		// 原因不明：FileWatchとDirWatchの両方が動いているから？
		}
		catch( final IOException ex ) {
			// MSG0008 = ﾌｧｲﾙが移動できませんでした。from=[{0}] to=[{1}]
//			MsgUtil.errPrintln( ex , "MSG0008" , from , to );
			LOGGER.warning( 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 );			// sufix を無条件につける為、existsCheck=false で登録
	}

	/**
	 * 単体ファイルをバックアップフォルダに移動します。
	 *
	 * これは、#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) ﾌｧｲﾙ名変更処理の修正
	 * @og.rev 7.2.5.0 (2020/06/01) toﾊﾟｽに、環境変数と日付文字列置換機能を追加します。
	 *
	 * @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 ;
		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() );
		}

		// 7.2.5.0 (2020/06/01) toﾊﾟｽに、環境変数と日付文字列置換機能を追加します。
		String toStr = movePath.toString();
	//	toStr = org.opengion.fukurou.util.StringUtil.replaceText( toStr , "{@ENV."  , "}" , System::getenv );				// 環境変数置換
	//	toStr = org.opengion.fukurou.util.StringUtil.replaceText( toStr , "{@DATE." , "}" , StringUtil::getTimeFormat );	// 日付文字列置換
		toStr = StringUtil.replaceText( toStr );				// 環境変数,日付文字列置換
		movePath = Paths.get( toStr );

//		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;
	}

	/**
	 * オリジナルファイルにバックアップファイルの行を追記します。
	 *
	 * オリジナルファイルに、バックアップファイルから読み取った行を追記していきます。
	 * 処理する条件は、オリジナルファイルとバックアップファイルが異なる場合のみ、実行されます。
	 * また、バックアップファイルから、追記する行で、COUNT,TIME,DATE の要素を持つ
	 * 行は、RPTファイルの先頭行なので、除外します。
	 *
	 * @og.rev 7.2.5.0 (2020/06/01) 新規追加。
	 * @og.rev 8.0.0.0 (2021/07/01) STATIC_LOCKのsynchronized作成
	 *
	 * @param orgPath	追加されるオリジナルのパス名
	 * @param bkup		行データを取り出すバックアップファイル
	 */
	public static void mergeFile( final Path orgPath , final Path bkup ) {
		if( exists( bkup ) && !bkup.equals( orgPath ) ) {			// 追記するバックアップファイルの存在を条件に加える。
			try {
				final List<String> lines = FileUtil.readAllLines( bkup );		// 1.4.0 (2019/10/01)
				// RPT,STS など、書き込み都度ヘッダー行を入れるファイルは、ヘッダー行を削除しておきます。
				if( lines.size() >= 2 ) {
					final String first = lines.get(0);	// RPTの先頭行で、COUNT,TIME,DATE を持っていれば、その行は削除します。
					if( first.contains( "COUNT" ) && first.contains( "DATE" ) && first.contains( "TIME" ) ) { lines.remove(0); }
				}										// 先頭行はトークン名
	// ※ lockSave がうまく動きません。
	//			if( useLock ) {
	//				lockSave( orgPath , lines , true );
	//			}
	//			else {
//					save( orgPath , lines , true );
					save( orgPath , lines , true , UTF_8 );
	//			}
				synchronized( STATIC_LOCK ) {
					Files.deleteIfExists( bkup );
				}
			}
			catch( final IOException ex ) {
				// MSG0003 = ﾌｧｲﾙがｵｰﾌﾟﾝ出来ませんでした。file=[{0}]
				throw MsgUtil.throwException( ex , "MSG0003" , bkup.toAbsolutePath().normalize() );
			}
		}
	}

	/**
	 * ファイルまたはフォルダ階層を削除します。
	 *
	 * これは、指定のパスが、フォルダの場合、階層すべてを削除します。
	 * 階層の途中にファイル等が存在していたとしても、削除します。
	 *
	 * Files.walkFileTree(Path,FileVisitor) を使用したファイル・ツリーの削除方式です。
	 *
	 * @og.rev 1.0.0 (2016/04/28) 新規追加
	 * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。
	 * @og.rev 8.0.0.0 (2021/07/01) STATIC_LOCKのsynchronized作成
	 *
	 * @param start	削除開始ファイル
	 * @throws	RuntimeException ファイル操作に失敗した場合
	 */
	public static void delete( final Path start ) {
		try {
//			if( Files.exists( start ) ) {
			if( exists( start ) ) {					// 7.2.5.0 (2020/06/01)
				synchronized( STATIC_LOCK ) {
					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を返します。
	 *
	 * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。
	 *
	 * @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 ) ) {
		if( exists( path ) ) {					// 仮想ﾌｫﾙﾀﾞなどの場合、実態が存在しないことがある。
			try{ Thread.sleep( sleep ); } catch( final InterruptedException ex ){}				// 先に、無条件に待ちます。
			try {
				if( !exists( path ) ) { return false; }											// 存在チェック。無ければ、false
				long size1 = Files.size( path );												// 7.3.1.3 (2021/03/09) forの前に移動
				for( int i=0; i<cnt; i++ ) {
//					if( Files.notExists( path ) ) { return false; }								// 存在チェック。無ければ、false
	//				if( !exists( path ) ) { break; }											// 存在チェック。無ければ、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
					if( !exists( path ) ) { break; }											// 存在チェック。無ければ、false
					final long size2 = Files.size( path );
					if( size1 != 0L && size1 == size2 ) { return true; }						// 安定した
					size1 = size2 ;																// 7.3.1.3 (2021/03/09) 次のチェックループ
				}
			}
			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} ミリ秒待機します。
	 *
	 * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。
	 * @og.rev 7.4.4.0 (2021/06/30) NoSuchFileException 時は、メッセージのみ表示する。
	 *
	 * @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 ) ) {
		if( exists( inPath ) ) {					// 7.2.5.0 (2020/06/01)
			// try-with-resources 文 (AutoCloseable)
			try( 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;		// 共有ロック獲得成功したので、ループから抜ける。
						}
					}
					// 要求された領域をオーバーラップするロックがこのJava仮想マシンにすでに確保されている場合。
					// または、このメソッド内でブロックされている別のスレッドが同じファイルのオーバーラップした領域をロックしようとしている場合
					catch( final OverlappingFileLockException ex ) {
		//				System.err.println( ex.getMessage() );
						if( i >= 3 ) {	// とりあえず３回までは、何も出さない
							// MSG0104 = 要求された領域のﾛｯｸは、このJava仮想ﾏｼﾝにすでに確保されています。 \n\tfile=[{0}]
		//					LOGGER.warning( ex , "MSG0104" , inPath );
							LOGGER.warning( "MSG0104" , inPath );					// 1.5.0 (2020/04/01) メッセージだけにしておきます。
						}
					}
					try{ Thread.sleep( LOCK_SLEEP_TIME ); } catch( final InterruptedException ex ){}
				}
			}
			// 7.4.4.0 (2021/06/30) NoSuchFileException 時は、メッセージのみ表示する。
			catch( final NoSuchFileException ex ) {
				// MSG0002 = ﾌｧｲﾙ/ﾌｫﾙﾀﾞが存在しません。\n\tfile=[{0}]
				LOGGER.warning( "MSG0002" , inPath );	// 原因不明：FileWatchとDirWatchの両方が動いているから？
			}
			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 );
			LOGGER.warning( "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 が呼ばれます。
	 *
	 * ファイルを順次読み込むため、内部メモリを圧迫しません。
	 *
	 * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。
	 *
	 * @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 ) ) {
		if( exists( inPath ) ) {					// 7.2.5.0 (2020/06/01)
			// try-with-resources 文 (AutoCloseable)
			String line = null;
			int no = 0;
	//		// こちらの方法では、lockForEach から来た場合に、エラーになります。
	//		try( BufferedReader reader = Files.newBufferedReader( inPath , chset ) ) {
			// 万一、コンストラクタでエラーが発生すると、リソース開放されない場合があるため、個別にインスタンスを作成しておきます。(念のため)
			try( FileInputStream   fin = new FileInputStream( inPath.toFile() );
				 InputStreamReader isr = new InputStreamReader( fin , chset );
				 BufferedReader reader = new BufferedReader( isr ) ) {

				while( ( line = reader.readLine() ) != null ) {
					// 1.2.0 (2018/09/01) UTF-8 BOM 対策
					// UTF-8 の BOM(0xEF 0xBB 0xBF) は、Java内部文字コードの UTF-16 BE では、0xFE 0xFF になる。
					// ファイルの先頭文字が、feff の場合は、その文字を削除します。
			//		if( no == 0 && !line.isEmpty() && Integer.toHexString(line.charAt(0)).equalsIgnoreCase("feff") ) {
					if( no == 0 && !line.isEmpty() && (int)line.charAt(0) == (int)'\ufeff' ) {
						// MSG0105 = 指定のﾌｧｲﾙは、UTF-8 BOM付きです。BOM無しﾌｧｲﾙで、運用してください。 \n\tfile=[{0}]
						System.out.println( MsgUtil.getMsg( "MSG0105" , inPath ) );
						line = line.substring(1);			// BOM の削除 : String#replace("\ufeff","") の方が良い？
					}

					action.accept( line );
					no++;
				}
			}
			catch( final IOException ex ) {
				// MSG0016 = ﾌｧｲﾙの行ﾃﾞｰﾀ読み込みに失敗しました。\n\tfile={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) 新規追加
	 * @og.rev 7.2.5.0 (2020/06/01) BOM付きファイルを append する場合の対処
	 * @og.rev 8.0.0.0 (2021/07/01) STATIC_LOCKのsynchronized作成
	 *
	 * @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,false );
		}

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

		synchronized( STATIC_LOCK ) {
			// try-with-resources 文 (AutoCloseable)
			try( PrintWriter out = new PrintWriter( Files.newBufferedWriter( savePath, chset , append ? APPEND : CREATE ) ) ) {
				 for( final String ln : lines ) {
	//				line = ln ;
					// 7.2.5.0 (2020/06/01) BOM付きファイルを append する場合の対処
					if( !ln.isEmpty() && (int)ln.charAt(0) == (int)'\ufeff' ) {
						line = ln.substring(1);			// BOM の削除 : String#replace("\ufeff","") の方が良い？
					}
					else {
						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 );
			}
		}
	}

	/**
	 * 指定のパスの最終更新日付を、文字列で返します。
	 * 文字列のフォーマット指定も可能です。
	 *
	 * パスが無い場合や、最終更新日付を、取得できない場合は、現在時刻をベースに返します。
	 *
	 * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。
	 * @og.rev 8.0.0.0 (2021/07/01) STATIC_LOCKのsynchronized作成
	 *
	 * @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 ) ) {
			if( exists( path ) ) {					// 7.2.5.0 (2020/06/01)
				synchronized( STATIC_LOCK ) {
					tempTime = Files.getLastModifiedTime( path ).toMillis();
				}
			}
		}
		catch( final IOException ex ) {
			// MSG0018 = ﾌｧｲﾙのﾀｲﾑｽﾀﾝﾌﾟの取得に失敗しました。file=[{0}]
//			MsgUtil.errPrintln( ex , "MSG0018" , path , ex.getMessage() );
			// MSG0018 = ﾌｧｲﾙのﾀｲﾑｽﾀﾝﾌﾟの取得に失敗しました。\n\tfile=[{0}]
			LOGGER.warning( ex , "MSG0018" , path );
		}
		if( tempTime == 0L ) {
			tempTime = System.currentTimeMillis();		// パスが無い場合や、エラー時は、現在時刻を使用
		}

		return StringUtil.getTimeFormat( tempTime , format );
	}

	/**
	 * ファイルからすべての行を読み取って、文字列のListとして返します。
	 *
	 * java.nio.file.Files#readAllLines​(Path ) と同等ですが、ﾌｧｲﾙが UTF-8 でない場合
	 * 即座にエラーにするのではなく、Windows-31J でも読み取りを試みます。
	 * それでもダメな場合は、IOException をスローします。
	 *
	 * @og.rev 7.2.5.0 (2020/06/01) Files.readAllLines の代用
	 * @og.rev 7.3.1.3 (2021/03/09) 読み込み処理全体に、try ～ catch を掛けておきます。
	 * @og.rev 8.0.0.0 (2021/07/01) STATIC_LOCKのsynchronized作成
	 *
	 * @param path		読み取り対象のPathオブジェクト
	 * @return	Listとしてファイルからの行
	 * @throws	IOException 読み取れない場合エラー
	 */
	public static List<String> readAllLines( final Path path ) throws IOException {
		// 7.3.1.3 (2021/03/09) 読み込み処理全体に、try ～ catch を掛けておきます。
		try {
			synchronized( STATIC_LOCK ) {
				try {
					return Files.readAllLines( path );				// StandardCharsets.UTF_8 指定と同等。
				}
				catch( final MalformedInputException ex ) {
					// MSG0030 = 指定のﾌｧｲﾙは、UTF-8でｵｰﾌﾟﾝ出来なかったため、Windows-31J で再実行します。\n\tfile=[{0}]
					LOGGER.warning( "MSG0030" , path );				// Exception は、引数に渡さないでおきます。

					return Files.readAllLines( path,WINDOWS_31J );
				}
			}
		}
		catch( final IOException ex ) {
			// MSG0005 = ﾌｫﾙﾀﾞのﾌｧｲﾙ読み込み時にｴﾗｰが発生しました。file=[{0}]
			throw MsgUtil.throwException( ex , "MSG0005" , path );
		}
	}

	/**
	 * Pathオブジェクトが存在しているかどうかを判定します。
	 *
	 * java.nio.file.Files#exists( Path ) を使用せず、java.io.File.exists() で判定します。
	 * https://codeday.me/jp/qa/20190302/349168.html
	 * ネットワークフォルダに存在するファイルの判定において、Files#exists( Path )と
	 * File.exists() の結果が異なることがあります。
	 * ここでは、File#exists() を使用して判定します。
	 *
	 * @og.rev 7.2.5.0 (2020/06/01) Files.exists の代用
	 *
	 * @param path		判定対象のPathオブジェクト
	 * @return	ファイルの存在チェック(あればtrue)
	 */
	public static boolean exists( final Path path ) {
	//	return Files.exists( path );
		return path != null && path.toFile().exists();
	}

	/**
	 * Pathオブジェクトのﾌｧｲﾙ名(getFileName().toString()) を取得します。
	 *
	 * Path#getFileName() では、結果が null になる場合もあり、そのままでは、toString() できません。
	 * また、引数の Path も null チェックが必要なので、それらを簡易的に行います。
	 * 何らかの結果が、null の場合は、""(空文字列)を返します。
	 *
	 * @og.rev 7.2.9.4 (2020/11/20) Path.getFileName().toString() の簡易版
	 *
	 * @param path		ﾌｧｲﾙ名取得元のPathオブジェクト(nullも可)
	 * @return	ﾌｧｲﾙ名(nullの場合は、空文字列)
	 * @og.rtnNotNull
	 */
	public static String pathFileName( final Path path ) {
		// 対応済み：spotbugs:null になっている可能性があるメソッドの戻り値を利用している
//		return path == null || path.getFileName() == null ? "" : path.getFileName().toString();

		if( path != null ) {
			final Path fname = path.getFileName();
			if( fname != null ) {
				return fname.toString();
			}
		}
		return "" ;
	}
}
