    001/*
    002 * Copyright (c) 2017 The openGion Project.
    003 *
    004 * Licensed under the Apache License, Version 2.0 (the "License");
    005 * you may not use this file except in compliance with the License.
    006 * You may obtain a copy of the License at
    007 *
    008 *     http://www.apache.org/licenses/LICENSE-2.0
    009 *
    010 * Unless required by applicable law or agreed to in writing, software
    011 * distributed under the License is distributed on an "AS IS" BASIS,
    012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
    013 * either express or implied. See the License for the specific language
    014 * governing permissions and limitations under the License.
    015 */
    016package org.opengion.fukurou.fileexec;
    017
    018import java.util.List;
    019import java.util.function.Consumer;
    020
    021import java.io.File;
    022import java.io.PrintWriter;
    023import java.io.BufferedReader;
    024import java.io.FileInputStream ;
    025import java.io.InputStreamReader ;
    026import java.io.IOException;
    027
    028import java.nio.file.Path;
    029import java.nio.file.Files;
    030import java.nio.file.Paths;
    031import java.nio.file.FileVisitor;
    032import java.nio.file.SimpleFileVisitor;
    033import java.nio.file.FileVisitResult;
    034import java.nio.file.StandardOpenOption;
    035import java.nio.file.StandardCopyOption;
    036import java.nio.file.attribute.BasicFileAttributes;
    037import java.nio.file.OpenOption;
    038import java.nio.file.NoSuchFileException;                                       // 7.2.5.0 (2020/06/01)
    039import java.nio.file.AccessDeniedException;                                     // 8.0.0.0 (2021/07/31)
    040import java.nio.channels.FileChannel;
    041import java.nio.channels.OverlappingFileLockException;
    042import java.nio.charset.Charset;
    043import java.nio.charset.MalformedInputException;                        // 7.2.5.0 (2020/06/01)
    044import static java.nio.charset.StandardCharsets.UTF_8;          // 7.2.5.0 (2020/06/01)
    045
    046/**
    047 * FileUtilは、共通的に使用されるファイル操作関連のメソッドを集約した、ユーティリティークラスです。
    048 *
    049 *<pre>
    050 * 読み込みチェックや、書き出しチェックなどの簡易的な処理をまとめているだけです。
    051 *
    052 *</pre>
    053 * @og.rev 7.0.0.0 (2017/07/07) 新規作成
    054 *
    055 * @version  7.0
    056 * @author   Kazuhiko Hasegawa
    057 * @since    JDK1.8,
    058 */
    059public final class FileUtil {
    060        private static final XLogger LOGGER= XLogger.getLogger( FileUtil.class.getSimpleName() );               // ログ出力
    061
    062        /** ファイルが安定するまでの待ち時間(ミリ秒) {@value} */
    063        public static final int STABLE_SLEEP_TIME  = 2000 ;     // ファイルが安定するまで、２秒待つ
    064        /** ファイルが安定するまでのリトライ回数 {@value} */
    065        public static final int STABLE_RETRY_COUNT = 10 ;       // ファイルが安定するまで、１０回リトライする。
    066
    067        /** ファイルロックの獲得までの待ち時間(ミリ秒) {@value} */
    068        public static final int LOCK_SLEEP_TIME  = 2000 ;       // ロックの獲得まで、２秒待つ
    069        /** ファイルロックの獲得までのリトライ回数 {@value} */
    070        public static final int LOCK_RETRY_COUNT = 10 ;         // ロックの獲得まで、１０回リトライする。
    071
    072        /** 日本語用の、Windows-31J の、Charset  */
    073        public static final Charset WINDOWS_31J = Charset.forName( "Windows-31J" );
    074
    075//      /** 日本語用の、UTF-8 の、Charset (Windows-31Jと同じように指定できるようにしておきます。)  */
    076//      public static final Charset UTF_8               = StandardCharsets.UTF_8;
    077
    078        private static final OpenOption[] CREATE = new OpenOption[] { StandardOpenOption.WRITE , StandardOpenOption.CREATE , StandardOpenOption.TRUNCATE_EXISTING };
    079        private static final OpenOption[] APPEND = new OpenOption[] { StandardOpenOption.WRITE , StandardOpenOption.CREATE , StandardOpenOption.APPEND };
    080
    081        private static final Object STATIC_LOCK = new Object();         // staticレベルのロック
    082
    083        /**
    084         * デフォルトコンストラクターをprivateにして、
    085         * オブジェクトの生成をさせないようにする。
    086         */
    087        private FileUtil() {}
    088
    089        /**
    090         * 引数の文字列を連結した読み込み用パスのチェックを行い、存在する場合は、そのパスオブジェクトを返します。
    091         *
    092         * Paths#get(String,String...) で作成したパスオブジェクトに存在チェックを加えたものです。
    093         * そのパスが存在しなければ、例外をThrowします。
    094         *
    095         * @og.rev 1.0.0 (2016/04/28) 新規追加
    096         * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。
    097         *
    098         * @param       first   パス文字列またはパス文字列の最初の部分
    099         * @param       more    結合してパス文字列を形成するための追加文字列
    100         * @return      指定の文字列を連結したパスオブジェクト
    101         * @throws      RuntimeException ファイル/フォルダは存在しない場合
    102         * @see         Paths#get(String,String...)
    103         */
    104        public static Path readPath( final String first , final String... more ) {
    105                final Path path = Paths.get( first,more ).toAbsolutePath().normalize() ;
    106
    107//              if( !Files.exists( path ) ) {
    108                if( !exists( path ) ) {                                                 // 7.2.5.0 (2020/06/01)
    109                        // MSG0002 = ﾌｧｲﾙ/ﾌｫﾙﾀﾞは存在しません。file=[{0}]
    110//                      throw MsgUtil.throwException( "MSG0002" , path );
    111                        final String errMsg = "FileUtil#readPath : Path=" + path ;
    112                        throw MsgUtil.throwException( "MSG0002" , errMsg );
    113                }
    114
    115                return path;
    116        }
    117
    118        /**
    119         * 引数の文字列を連結した書き込み用パスを作成します。
    120         *
    121         * Paths#get(String,String...) で作成したパスオブジェクトに存在チェックを加え、
    122         * そのパスが存在しなければ、作成します。
    123         * パスが、フォルダの場合は、そのまま作成し、ファイルの場合は、親フォルダまでを作成します。
    124         * パスがフォルダかファイルかの区別は、拡張子があるかどうかで判定します。
    125         *
    126         * @og.rev 1.0.0 (2016/04/28) 新規追加
    127         *
    128         * @param       first   パス文字列またはパス文字列の最初の部分
    129         * @param       more    結合してパス文字列を形成するための追加文字列
    130         * @return      指定の文字列を連結したパスオブジェクト
    131         * @throws      RuntimeException ファイル/フォルダが作成できなかった場合
    132         * @see         Paths#get(String,String...)
    133         */
    134        public static Path writePath( final String first , final String... more ) {
    135                final Path path = Paths.get( first,more ).toAbsolutePath().normalize() ;
    136
    137                mkdirs( path,false );
    138
    139                return path;
    140        }
    141
    142        /**
    143         * ファイルオブジェクトを作成します。
    144         *
    145         * 通常は、フォルダ＋ファイル名で、新しいファイルオブジェクトを作成します。
    146         * ここでは、第２引数のファイル名に、絶対パスを指定した場合は、第１引数の
    147         * フォルダを使用せず、ファイル名だけで、ファイルオブジェクトを作成します。
    148         * 第２引数のファイル名が、null か、ゼロ文字列の場合は、第１引数の
    149         * フォルダを返します。
    150         *
    151         * @og.rev 7.2.1.0 (2020/03/13) isAbsolute(String)を利用します。
    152         *
    153         * @param path  基準となるフォルダ(ファイルの場合は、親フォルダ基準)
    154         * @param fname ファイル名(絶対パス、または、相対パス)
    155         * @return 合成されたファイルオブジェクト
    156         */
    157        public static Path newPath( final Path path , final String fname ) {
    158                if( fname == null || fname.isEmpty() ) {
    159                        return path;
    160                }
    161//              else if( fname.charAt(0) == '/'                                                 ||              // 実フォルダが UNIX
    162//                               fname.charAt(0) == '\\'                                                ||              // 実フォルダが ネットワークパス
    163//                               fname.length() > 1 && fname.charAt(1) == ':' ) {               // 実フォルダが Windows
    164                else if( isAbsolute( fname ) ) {
    165                        return new File( fname ).toPath();
    166                }
    167                else {
    168                        return path.resolve( fname );
    169                }
    170        }
    171
    172        /**
    173         * ファイルアドレスが絶対パスかどうか[絶対パス:true]を判定します。
    174         *
    175         * ファイル名が、絶対パス('/' か、'\\' か、2文字目が ':' の場合)かどうかを
    176         * 判定して、絶対パスの場合は、true を返します。
    177         * それ以外(nullやゼロ文字列も含む)は、false になります。
    178         *
    179         * @og.rev 7.2.1.0 (2020/03/13) 新規追加
    180         *
    181         * @param fname ファイルパスの文字列(絶対パス、相対パス、null、ゼロ文字列)
    182         * @return 絶対パスの場合は true
    183         */
    184        public static boolean isAbsolute( final String fname ) {
    185//              return fname != null && (
    186                return fname != null && !fname.isEmpty() && (
    187                                   fname.charAt(0) == '/'                                                               // 実フォルダが UNIX
    188                                || fname.charAt(0) == '\\'                                                              // 実フォルダが ネットワークパス
    189                                || fname.length() > 1 && fname.charAt(1) == ':' );              // 実フォルダが Windows
    190        }
    191
    192        /**
    193         * 引数のファイルパスを親階層を含めて生成します。
    194         *
    195         * すでに存在している場合や作成が成功した場合は、true を返します。
    196         * 作成に失敗した場合は、false です。
    197         * 指定のファイルパスは、フォルダであることが前提ですが、簡易的に
    198         * ファイルの場合は、その親階層のフォルダを作成します。
    199         * ファイルかフォルダの判定は、拡張子があるか、ないかで判定します。
    200         *
    201         * @og.rev 1.0.0 (2016/04/28) 新規追加
    202         * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。
    203         * @og.rev 8.0.0.0 (2021/07/01) STATIC_LOCKのsynchronized作成
    204         *
    205         * @param       target  ターゲットのファイルパス
    206         * @param       parentCheck     先に親ﾌｫﾙﾀﾞの作成を行うかどうか(true:行う)
    207         * @throws      RuntimeException フォルダの作成に失敗した場合
    208         */
    209//      public static void mkdirs( final Path target ) {
    210        public static void mkdirs( final Path target,final boolean parentCheck ) {
    211//              if( Files.notExists( target ) ) {               // 存在しない場合
    212                if( !exists( target ) ) {                               // 存在しない場合 7.2.5.0 (2020/06/01)
    213                        // 6.9.8.0 (2018/05/28) FindBugs:null になっている可能性があるメソッドの戻り値を利用している
    214//                      final boolean isFile = target.getFileName().toString().contains( "." );         // ファイルかどうかは、拡張子の有無で判定する。
    215
    216                        final Path tgtName = target.getFileName();
    217                        if( tgtName == null ) {
    218                                // MSG0007 = ﾌｧｲﾙ/ﾌｫﾙﾀﾞの作成に失敗しました。\n\tdir=[{0}]
    219                                throw MsgUtil.throwException( "MSG0007" , target.toString() );
    220                        }
    221
    222                        final boolean isFile = tgtName.toString().contains( "." );                                      // ファイルかどうかは、拡張子の有無で判定する。
    223//                      final Path dir = isFile ? target.toAbsolutePath().getParent() : target ;        // ファイルなら、親フォルダを取り出す。
    224                        final Path dir = isFile ? target.getParent() : target ;                                         // ファイルなら、親フォルダを取り出す。
    225                        if( dir == null ) {
    226                                // MSG0007 = ﾌｧｲﾙ/ﾌｫﾙﾀﾞの作成に失敗しました。\n\tdir=[{0}]
    227                                throw MsgUtil.throwException( "MSG0007" , target.toString() );
    228                        }
    229
    230//                      if( Files.notExists( dir ) ) {          // 存在しない場合
    231                        if( !exists( dir ) ) {                          // 存在しない場合 7.2.5.0 (2020/06/01)
    232                                try {
    233                                        synchronized( STATIC_LOCK ) {                   // 8.0.0.0 (2021/07/01) 意味があるかどうかは不明
    234                                                Files.createDirectories( dir );
    235                                        }
    236                                }
    237                                catch( final IOException ex ) {
    238                                        // MSG0007 = ﾌｧｲﾙ/ﾌｫﾙﾀﾞの作成に失敗しました。dir=[{0}]
    239                                        throw MsgUtil.throwException( ex , "MSG0007" , dir );
    240                                }
    241                        }
    242                }
    243        }
    244
    245        /**
    246         * 単体ファイルをコピーします。
    247         *
    248         * コピー先がなければ、コピー先のフォルダ階層を作成します。
    249         * コピー先がフォルダの場合は、コピー元と同じファイル名で、コピーします。
    250         * コピー先のファイルがすでに存在する場合は、上書きされますので、
    251         * 必要であれば、先にバックアップしておいて下さい。
    252         *
    253         * @og.rev 1.0.0 (2016/04/28) 新規追加
    254         *
    255         * @param from  コピー元となるファイル
    256         * @param to    コピー先となるファイル
    257         * @throws      RuntimeException ファイル操作に失敗した場合
    258         * @see         #copy(Path,Path,boolean)
    259         */
    260        public static void copy( final Path from , final Path to ) {
    261                copy( from,to,false );
    262        }
    263
    264        /**
    265         * パスの共有ロックを指定した、単体ファイルをコピーします。
    266         *
    267         * コピー先がなければ、コピー先のフォルダ階層を作成します。
    268         * コピー先がフォルダの場合は、コピー元と同じファイル名で、コピーします。
    269         * コピー先のファイルがすでに存在する場合は、上書きされますので、
    270         * 必要であれば、先にバックアップしておいて下さい。
    271         *
    272         * ※ copy に関しては、コピー時間を最小化する意味で、synchronized しています。
    273         *
    274         * @og.rev 1.0.0 (2016/04/28) 新規追加
    275         * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。
    276         * @og.rev 7.3.1.3 (2021/03/09) 処理の直前にロックを掛けてから存在チェックを行います。
    277         *
    278         * @param from  コピー元となるファイル
    279         * @param to    コピー先となるファイル
    280         * @param useLock       パスを共有ロックするかどうか
    281         * @throws      RuntimeException ファイル操作に失敗した場合
    282         * @see         #copy(Path,Path)
    283         */
    284        public static void copy( final Path from , final Path to , final boolean useLock ) {
    285//              if( Files.exists( from ) ) {
    286                if( exists( from ) ) {                                                  // 7.2.5.0 (2020/06/01)
    287                        mkdirs( to,false );
    288
    289                        // 6.9.8.0 (2018/05/28) FindBugs:null になっている可能性があるメソッドの戻り値を利用している
    290//                      final boolean isFile = to.getFileName().toString().contains( "." );                     // ファイルかどうかは、拡張子の有無で判定する。
    291
    292                        final Path toName = to.getFileName();
    293                        if( toName == null ) {
    294                                // MSG0008 = ﾌｧｲﾙが移動できませんでした。\n\tfrom=[{0}] to=[{1}]
    295                                throw MsgUtil.throwException( "MSG0008" , from.toString() , to.toString() );
    296                        }
    297
    298                        final boolean isFile = toName.toString().contains( "." );               // ファイルかどうかは、拡張子の有無で判定する。
    299
    300                        // コピー先がフォルダの場合は、コピー元と同じ名前のファイルにする。
    301                        final Path save = isFile ? to : to.resolve( from.getFileName() );
    302
    303                        synchronized( STATIC_LOCK ) {
    304                                // 7.3.1.3 (2021/03/09) 処理の直前にロックを掛けてから存在チェックを行います。
    305                                if( exists( from ) ) {
    306                                        if( useLock ) {
    307                                                lockPath( from , in -> localCopy( in , save ) );
    308                                        }
    309                                        else {
    310                                                localCopy( from , save );
    311                                        }
    312                                }
    313                        }
    314                }
    315                else {
    316                        // 7.2.5.0 (2020/06/01)
    317                        // MSG0002 = ﾌｧｲﾙ/ﾌｫﾙﾀﾞが存在しません。file=[{0}]
    318//                      MsgUtil.errPrintln( "MSG0002" , from );
    319                        final String errMsg = "FileUtil#copy : from=" + from ;
    320                        LOGGER.warning( "MSG0002" , errMsg );
    321                }
    322        }
    323
    324        /**
    325         * 単体ファイルをコピーします。
    326         *
    327         * これは、IOException の処理と、直前の存在チェックをまとめたメソッドです。
    328         *
    329         * @og.rev 1.0.0 (2016/04/28) 新規追加
    330         * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。
    331         * @og.rev 7.3.1.3 (2021/03/09) 処理の直前にロックを掛けてから存在チェックを行います。
    332         * @og.rev 7.4.4.0 (2021/06/30) copy/move がきちんとできたか確認します(ﾌｧｲﾙｻｲｽﾞﾁｪｯｸ)
    333         *
    334         * @param from  コピー元となるファイル
    335         * @param to    コピー先となるファイル
    336         */
    337        private static void localCopy( final Path from , final Path to ) {
    338                try {
    339                        // 直前に存在チェックを行います。
    340//                      if( Files.exists( from ) ) {
    341                        // 7.3.1.3 (2021/03/09) 処理の直前にロックを掛けてから存在チェックを行います。
    342        //              synchronized( STATIC_LOCK ) {                                           // 7.4.4.0 (2021/06/30) 意味がないので外す。
    343                                if( exists( from ) ) {                                                  // 7.2.5.0 (2020/06/01)
    344                                        final long fromSize = Files.size(from);         // 7.4.4.0 (2021/06/30)
    345                                        Files.copy( from , to , StandardCopyOption.REPLACE_EXISTING );
    346
    347                                        // 7.4.4.0 (2021/06/30) copy/move がきちんとできたか確認します(ﾌｧｲﾙｻｲｽﾞﾁｪｯｸ)
    348                                        for( int i=0; i<STABLE_RETRY_COUNT; i++ ) {
    349                                                // 8.0.0.0 (2021/07/31) Avoid if (x != y) ..; else ..;
    350                                                if( fromSize == Files.size(to) ) {
    351                                                        return ;
    352                                                }
    353                                                else {
    354                                                        try{ Thread.sleep( STABLE_SLEEP_TIME ); } catch( final InterruptedException ex ){}
    355                                                }
    356
    357//                                              final long toSize = Files.size(to);
    358//                                              if( fromSize != toSize ) {
    359//                                                      try{ Thread.sleep( STABLE_SLEEP_TIME ); } catch( final InterruptedException ex ){}
    360//                                              }
    361//                                              else {
    362//                                                      break;
    363//                                              }
    364                                        }
    365                                }
    366        //              }
    367                }
    368                catch( final NoSuchFileException ex ) {                         // 8.0.0.0 (2021/07/31)
    369                        // MSG0002 = ﾌｧｲﾙ/ﾌｫﾙﾀﾞが存在しません。\n\tfile=[{0}]
    370                        LOGGER.warning( "MSG0002" , from );
    371                        // MSG0012 = ﾌｧｲﾙがｺﾋﾟｰできませんでした。from=[{0}] to=[{1}]
    372                        LOGGER.warning( "MSG0012" , from , to );                // 原因不明：FileWatchとDirWatchの両方が動いているから？
    373                }
    374                catch( final IOException ex ) {
    375                        // MSG0012 = ﾌｧｲﾙがｺﾋﾟｰできませんでした。from=[{0}] to=[{1}]
    376//                      MsgUtil.errPrintln( ex , "MSG0012" , from , to );
    377                        LOGGER.warning( ex , "MSG0012" , from , to );
    378                }
    379        }
    380
    381        /**
    382         * 単体ファイルを移動します。
    383         *
    384         * 移動先がなければ、移動先のフォルダ階層を作成します。
    385         * 移動先がフォルダの場合は、移動元と同じファイル名で、移動します。
    386         * 移動先のファイルがすでに存在する場合は、上書きされますので、
    387         * 必要であれば、先にバックアップしておいて下さい。
    388         *
    389         * @og.rev 1.0.0 (2016/04/28) 新規追加
    390         *
    391         * @param from  移動元となるファイル
    392         * @param to    移動先となるファイル
    393         * @throws      RuntimeException ファイル操作に失敗した場合
    394         * @see         #move(Path,Path,boolean)
    395         */
    396        public static void move( final Path from , final Path to ) {
    397                move( from,to,false );
    398        }
    399
    400        /**
    401         * パスの共有ロックを指定した、単体ファイルを移動します。
    402         *
    403         * 移動先がなければ、移動先のフォルダ階層を作成します。
    404         * 移動先がフォルダの場合は、移動元と同じファイル名で、移動します。
    405         * 移動先のファイルがすでに存在する場合は、上書きされますので、
    406         * 必要であれば、先にバックアップしておいて下さい。
    407         *
    408         * ※ move に関しては、ムーブ時間を最小化する意味で、synchronized しています。
    409         *
    410         * @og.rev 1.0.0 (2016/04/28) 新規追加
    411         * @og.rev 7.2.1.0 (2020/03/13) from,to が null の場合、処理しない。
    412         * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。
    413         * @og.rev 7.3.1.3 (2021/03/09) 処理の直前にロックを掛けてから存在チェックを行います。
    414         *
    415         * @param from  移動元となるファイル
    416         * @param to    移動先となるファイル
    417         * @param useLock       パスを共有ロックするかどうか
    418         * @throws      RuntimeException ファイル操作に失敗した場合
    419         * @see         #move(Path,Path)
    420         */
    421        public static void move( final Path from , final Path to , final boolean useLock ) {
    422                if( from == null || to == null ) { return; }                    // 7.2.1.0 (2020/03/13)
    423
    424//              if( Files.exists( from ) ) {
    425                if( exists( from ) ) {                                  // 1.4.0 (2019/09/01)
    426                        mkdirs( to,false );
    427
    428                        // ファイルかどうかは、拡張子の有無で判定する。
    429                        // 6.9.8.0 (2018/05/28) FindBugs:null になっている可能性があるメソッドの戻り値を利用している
    430//                      final boolean isFile = to.getFileName().toString().contains( "." );
    431                        final Path toName = to.getFileName();
    432                        if( toName == null ) {
    433                                // MSG0008 = ﾌｧｲﾙが移動できませんでした。\n\tfrom=[{0}] to=[{1}]
    434                                throw MsgUtil.throwException( "MSG0008" , to.toString() );
    435                        }
    436
    437                        final boolean isFile = toName.toString().contains( "." );               // ファイルかどうかは、拡張子の有無で判定する。
    438
    439                        // 移動先がフォルダの場合は、コピー元と同じ名前のファイルにする。
    440                        final Path save = isFile ? to : to.resolve( from.getFileName() );
    441
    442                        synchronized( STATIC_LOCK ) {
    443                                // 7.3.1.3 (2021/03/09) 処理の直前にロックを掛けてから存在チェックを行います。
    444                                if( exists( from ) ) {
    445                                        if( useLock ) {
    446                                                lockPath( from , in -> localMove( in , save ) );
    447                                        }
    448                                        else {
    449                                                localMove( from , save );
    450                                        }
    451                                }
    452                        }
    453                }
    454                else {
    455                        // MSG0002 = ﾌｧｲﾙ/ﾌｫﾙﾀﾞが存在しません。file=[{0}]
    456//                      MsgUtil.errPrintln( "MSG0002" , from );
    457                        final String errMsg = "FileUtil#move : from=" + from ;
    458                        LOGGER.warning( "MSG0002" , errMsg );
    459                }
    460        }
    461
    462        /**
    463         * 単体ファイルを移動します。
    464         *
    465         * これは、IOException の処理と、直前の存在チェックをまとめたメソッドです。
    466         *
    467         * @og.rev 1.0.0 (2016/04/28) 新規追加
    468         * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。
    469         * @og.rev 7.3.1.3 (2021/03/09) 処理の直前にロックを掛けてから存在チェックを行います。
    470         * @og.rev 7.4.4.0 (2021/06/30) copy/move がきちんとできたか確認します(ﾌｧｲﾙｻｲｽﾞﾁｪｯｸ)
    471         *
    472         * @param from  移動元となるファイル
    473         * @param to    移動先となるファイル
    474         */
    475        private static void localMove( final Path from , final Path to ) {
    476                try {
    477        //              synchronized( from ) {
    478                                // 直前に存在チェックを行います。
    479//                              if( Files.exists( from ) ) {
    480                                // 7.3.1.3 (2021/03/09) 処理の直前にロックを掛けてから存在チェックを行います。
    481        //                      synchronized( STATIC_LOCK ) {                                           // 7.4.4.0 (2021/06/30) 意味がないので外す。
    482                                        if( exists( from ) ) {                                                  // このメソッドの結果がすぐに古くなることに注意してください。
    483                                                // CopyOption に、StandardCopyOption.ATOMIC_MOVE を指定すると、別サーバー等へのMOVEは、出来なくなります。
    484                                        //      try{ Thread.sleep( 2000 ); } catch( final InterruptedException ex ){}                           // 先に、無条件に待ちます。
    485                                                final long fromSize = Files.size(from);         // 7.4.4.0 (2021/06/30)
    486                                                Files.move( from , to , StandardCopyOption.REPLACE_EXISTING );
    487
    488                                                // 7.4.4.0 (2021/06/30) copy/move がきちんとできたか確認します(ﾌｧｲﾙｻｲｽﾞﾁｪｯｸ)
    489                                                for( int i=0; i<STABLE_RETRY_COUNT; i++ ) {
    490                                                        // 8.0.0.0 (2021/07/31) Avoid if (x != y) ..; else ..;
    491                                                        if( fromSize == Files.size(to) ) {
    492                                                                return ;
    493                                                        }
    494                                                        else {
    495                                                                try{ Thread.sleep( STABLE_SLEEP_TIME ); } catch( final InterruptedException ex ){}
    496                                                        }
    497
    498//                                                      final long toSize = Files.size(to);
    499//                                                      if( fromSize != toSize ) {
    500//                                                              try{ Thread.sleep( STABLE_SLEEP_TIME ); } catch( final InterruptedException ex ){}
    501//                                                      }
    502//                                                      else {
    503//                                                              break;
    504//                                                      }
    505                                                }
    506                                        }
    507        //                      }
    508        //              }
    509                }
    510                catch( final AccessDeniedException ex ) {                               // 8.0.0.0 (2021/07/31)
    511                        // MSG0034 = ﾌｧｲﾙｻｲｽﾞの取得ができませんでした。\n\tfile=[{0}]
    512                        LOGGER.warning( "MSG0034" , from );
    513                        // MSG0008 = ﾌｧｲﾙが移動できませんでした。from=[{0}] to=[{1}]
    514                        LOGGER.warning( "MSG0008" , from , to );                // 原因不明：FileWatchとDirWatchの両方が動いているから？
    515                }
    516                catch( final NoSuchFileException ex ) {                         // 7.2.5.0 (2020/06/01)
    517                        // MSG0002 = ﾌｧｲﾙ/ﾌｫﾙﾀﾞが存在しません。\n\tfile=[{0}]
    518                        LOGGER.warning( "MSG0002" , from );
    519                        // MSG0008 = ﾌｧｲﾙが移動できませんでした。from=[{0}] to=[{1}]
    520                        LOGGER.warning( "MSG0008" , from , to );                // 原因不明：FileWatchとDirWatchの両方が動いているから？
    521                }
    522                catch( final IOException ex ) {
    523                        // MSG0008 = ﾌｧｲﾙが移動できませんでした。from=[{0}] to=[{1}]
    524//                      MsgUtil.errPrintln( ex , "MSG0008" , from , to );
    525                        LOGGER.warning( ex , "MSG0008" , from , to );
    526                }
    527        }
    528
    529        /**
    530         * 単体ファイルをバックアップフォルダに移動します。
    531         *
    532         * これは、#backup( from,to,true,false,sufix ); と同じ処理を実行します。
    533         *
    534         * 移動先は、フォルダ指定で、ファイル名は存在チェックせずに、必ず変更します。
    535         * その際、移動元＋サフィックス のファイルを作成します。
    536         * ファイルのロックを行います。
    537         *
    538         * @og.rev 1.0.0 (2016/04/28) 新規追加
    539         *
    540         * @param from  移動元となるファイル
    541         * @param to    移動先となるフォルダ(nullの場合は、移動元と同じフォルダ)
    542         * @param sufix バックアップファイル名の後ろに付ける文字列
    543         * @return      バックアップしたファイルパス。
    544         * @throws      RuntimeException ファイル操作に失敗した場合
    545         * @see #backup( Path , Path , boolean , boolean , String )
    546         */
    547        public static Path backup( final Path from , final Path to , final String sufix ) {
    548                return backup( from,to,true,false,sufix );                      // sufix を無条件につける為、existsCheck=false で登録
    549        }
    550
    551        /**
    552         * 単体ファイルをバックアップフォルダに移動します。
    553         *
    554         * これは、#backup( from,to,true,true ); と同じ処理を実行します。
    555         *
    556         * 移動先は、フォルダ指定で、ファイル名は存在チェックの上で、無ければ移動、
    557         * あれば、移動元＋時間情報 のファイルを作成します。
    558         * ファイルのロックを行います。
    559         * 移動先を指定しない(=null)場合は、自分自身のフォルダでの、ファイル名変更になります。
    560         *
    561         * @og.rev 1.0.0 (2016/04/28) 新規追加
    562         *
    563         * @param from  移動元となるファイル
    564         * @param to    移動先となるフォルダ(nullの場合は、移動元と同じフォルダ)
    565         * @return      バックアップしたファイルパス。
    566         * @throws      RuntimeException ファイル操作に失敗した場合
    567         * @see #backup( Path , Path , boolean , boolean , String )
    568         */
    569        public static Path backup( final Path from , final Path to ) {
    570                return backup( from,to,true,true,null );
    571        }
    572
    573        /**
    574         * パスの共有ロックを指定して、単体ファイルをバックアップフォルダに移動します。
    575         *
    576         * 移動先のファイル名は、existsCheckが、trueの場合は、移動先のファイル名をチェックして、
    577         * 存在しなければ、移動元と同じファイル名で、バックアップフォルダに移動します。
    578         * 存在すれば、ファイル名＋サフィックス のファイルを作成します。(拡張子より後ろにサフィックスを追加します。)
    579         * existsCheckが、false の場合は、無条件に、移動元のファイル名に、サフィックスを追加します。
    580         * サフィックスがnullの場合は、時間情報になります。
    581         * 移動先を指定しない(=null)場合は、自分自身のフォルダでの、ファイル名変更になります。
    582         *
    583         * @og.rev 1.0.0 (2016/04/28) 新規追加
    584         * @og.rev 6.9.8.0 (2018/05/28) FindBugs:null になっている可能性があるメソッドの戻り値を利用している
    585         * @og.rev 7.2.1.0 (2020/03/13) ﾌｧｲﾙ名変更処理の修正
    586         * @og.rev 7.2.5.0 (2020/06/01) toﾊﾟｽに、環境変数と日付文字列置換機能を追加します。
    587         *
    588         * @param from  移動元となるファイル
    589         * @param to    移動先となるフォルダ(nullの場合は、移動元と同じフォルダ)
    590         * @param useLock       パスを共有ロックするかどうか
    591         * @param existsCheck   移動先のファイル存在チェックを行うかどうか(true:行う/false:行わない)
    592         * @param sufix バックアップファイル名の後ろに付ける文字列
    593         *
    594         * @return      バックアップしたファイルパス。
    595         * @throws      RuntimeException ファイル操作に失敗した場合
    596         * @see #backup( Path , Path )
    597         */
    598        public static Path backup( final Path from , final Path to , final boolean useLock , final boolean existsCheck , final String sufix ) {
    599//              final Path movePath = to == null ? from.getParent() : to ;
    600                Path movePath = to == null ? from.getParent() : to ;
    601
    602                // 6.9.8.0 (2018/05/28) FindBugs:null になっている可能性があるメソッドの戻り値を利用している
    603                if( movePath == null ) {
    604                        // MSG0007 = ﾌｧｲﾙ/ﾌｫﾙﾀﾞの作成に失敗しました。\n\tdir=[{0}]
    605                        throw MsgUtil.throwException( "MSG0007" , from.toString() );
    606                }
    607
    608                // 7.2.5.0 (2020/06/01) toﾊﾟｽに、環境変数と日付文字列置換機能を追加します。
    609                String toStr = movePath.toString();
    610        //      toStr = org.opengion.fukurou.util.StringUtil.replaceText( toStr , "{@ENV."  , "}" , System::getenv );                           // 環境変数置換
    611        //      toStr = org.opengion.fukurou.util.StringUtil.replaceText( toStr , "{@DATE." , "}" , StringUtil::getTimeFormat );        // 日付文字列置換
    612                toStr = StringUtil.replaceText( toStr );                                // 環境変数,日付文字列置換
    613                movePath = Paths.get( toStr );
    614
    615//              final String fileName = from.getFileName().toString();
    616                final Path      fName = from.getFileName();
    617                if( fName == null ) {
    618                        // MSG0002 = ﾌｧｲﾙ/ﾌｫﾙﾀﾞが存在しません。\n\tfile=[{0}]
    619                        throw MsgUtil.throwException( "MSG0002" , from.toString() );
    620                }
    621
    622//              final Path      moveFile = movePath.resolve( fileName );                                        // 移動先のファイルパスを構築
    623                final Path      moveFile = movePath.resolve( fName );                                           // 移動先のファイルパスを構築
    624
    625//              final boolean isExChk = existsCheck && Files.notExists( moveFile );             // 存在しない場合、true。存在するか、不明の場合は、false。
    626
    627                final Path bkupPath;
    628//              if( isExChk ) {
    629                if( existsCheck && Files.notExists( moveFile ) ) {                              // 存在しない場合、true。存在するか、不明の場合は、false。
    630                        bkupPath = moveFile;
    631                }
    632                else {
    633                        final String fileName = fName.toString();                                       // from パスの名前
    634                        final int ad = fileName.lastIndexOf( '.' );                                     // ピリオドの手前に、タイムスタンプを入れる。
    635                        // 7.2.1.0 (2020/03/13) ﾌｧｲﾙ名変更処理の修正
    636                        if( ad > 0 ) {
    637                                bkupPath = movePath.resolve(
    638                                                                fileName.substring( 0,ad )
    639                                                                + "_"
    640                                                                + StringUtil.nval( sufix , StringUtil.getTimeFormat() )
    641                                                                + fileName.substring( ad )                              // ad 以降なので、ピリオドも含む
    642                                                );
    643                        }
    644                        else {
    645                                bkupPath = null;
    646                        }
    647                }
    648
    649                move( from,bkupPath,useLock );
    650
    651                return bkupPath;
    652        }
    653
    654        /**
    655         * オリジナルファイルにバックアップファイルの行を追記します。
    656         *
    657         * オリジナルファイルに、バックアップファイルから読み取った行を追記していきます。
    658         * 処理する条件は、オリジナルファイルとバックアップファイルが異なる場合のみ、実行されます。
    659         * また、バックアップファイルから、追記する行で、COUNT,TIME,DATE の要素を持つ
    660         * 行は、RPTファイルの先頭行なので、除外します。
    661         *
    662         * @og.rev 7.2.5.0 (2020/06/01) 新規追加。
    663         * @og.rev 8.0.0.0 (2021/07/01) STATIC_LOCKのsynchronized作成
    664         *
    665         * @param orgPath       追加されるオリジナルのパス名
    666         * @param bkup          行データを取り出すバックアップファイル
    667         */
    668        public static void mergeFile( final Path orgPath , final Path bkup ) {
    669                if( exists( bkup ) && !bkup.equals( orgPath ) ) {                       // 追記するバックアップファイルの存在を条件に加える。
    670                        try {
    671                                final List<String> lines = FileUtil.readAllLines( bkup );               // 1.4.0 (2019/10/01)
    672                                // RPT,STS など、書き込み都度ヘッダー行を入れるファイルは、ヘッダー行を削除しておきます。
    673                                if( lines.size() >= 2 ) {
    674                                        final String first = lines.get(0);      // RPTの先頭行で、COUNT,TIME,DATE を持っていれば、その行は削除します。
    675                                        if( first.contains( "COUNT" ) && first.contains( "DATE" ) && first.contains( "TIME" ) ) { lines.remove(0); }
    676                                }                                                                               // 先頭行はトークン名
    677        // ※ lockSave がうまく動きません。
    678        //                      if( useLock ) {
    679        //                              lockSave( orgPath , lines , true );
    680        //                      }
    681        //                      else {
    682//                                      save( orgPath , lines , true );
    683                                        save( orgPath , lines , true , UTF_8 );
    684        //                      }
    685                                synchronized( STATIC_LOCK ) {
    686                                        Files.deleteIfExists( bkup );
    687                                }
    688                        }
    689                        catch( final IOException ex ) {
    690                                // MSG0003 = ﾌｧｲﾙがｵｰﾌﾟﾝ出来ませんでした。file=[{0}]
    691                                throw MsgUtil.throwException( ex , "MSG0003" , bkup.toAbsolutePath().normalize() );
    692                        }
    693                }
    694        }
    695
    696        /**
    697         * ファイルまたはフォルダ階層を削除します。
    698         *
    699         * これは、指定のパスが、フォルダの場合、階層すべてを削除します。
    700         * 階層の途中にファイル等が存在していたとしても、削除します。
    701         *
    702         * Files.walkFileTree(Path,FileVisitor) を使用したファイル・ツリーの削除方式です。
    703         *
    704         * @og.rev 1.0.0 (2016/04/28) 新規追加
    705         * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。
    706         * @og.rev 8.0.0.0 (2021/07/01) STATIC_LOCKのsynchronized作成
    707         *
    708         * @param start 削除開始ファイル
    709         * @throws      RuntimeException ファイル操作に失敗した場合
    710         */
    711        public static void delete( final Path start ) {
    712                try {
    713//                      if( Files.exists( start ) ) {
    714                        if( exists( start ) ) {                                 // 7.2.5.0 (2020/06/01)
    715                                synchronized( STATIC_LOCK ) {
    716                                        Files.walkFileTree( start, DELETE_VISITOR );
    717                                }
    718                        }
    719                }
    720                catch( final IOException ex ) {
    721                        // MSG0011 = ﾌｧｲﾙが削除できませんでした。file=[{0}]
    722                        throw MsgUtil.throwException( ex , "MSG0011" , start );
    723                }
    724        }
    725
    726        /**
    727         * delete(Path)で使用する、Files.walkFileTree の引数の FileVisitor オブジェクトです。
    728         *
    729         * staticオブジェクトを作成しておき、使いまわします。
    730         */
    731        private static final FileVisitor<Path> DELETE_VISITOR = new SimpleFileVisitor<Path>() {
    732                /**
    733                 * ディレクトリ内のファイルに対して呼び出されます。
    734                 *
    735                 * @param file  ファイルへの参照
    736                 * @param attrs ファイルの基本属性
    737                 * @throws      IOException 入出力エラーが発生した場合
    738                 */
    739                @Override
    740                public FileVisitResult visitFile( final Path file, final BasicFileAttributes attrs ) throws IOException {
    741                        Files.deleteIfExists( file );           // ファイルが存在する場合は削除
    742                        return FileVisitResult.CONTINUE;
    743                }
    744
    745                /**
    746                 * ディレクトリ内のエントリ、およびそのすべての子孫がビジットされたあとにそのディレクトリに対して呼び出されます。
    747                 *
    748                 * @param dir   ディレクトリへの参照
    749                 * @param ex    エラーが発生せずにディレクトリの反復が完了した場合はnull、そうでない場合はディレクトリの反復が早く完了させた入出力例外
    750                 * @throws      IOException 入出力エラーが発生した場合
    751                 */
    752                @Override
    753                public FileVisitResult postVisitDirectory( final Path dir, final IOException ex ) throws IOException {
    754                        if( ex == null ) {
    755                                Files.deleteIfExists( dir );            // ファイルが存在する場合は削除
    756                                return FileVisitResult.CONTINUE;
    757                        } else {
    758                                // directory iteration failed
    759                                throw ex;
    760                        }
    761                }
    762        };
    763
    764        /**
    765         * 指定のパスのファイルが、書き込まれている途中かどうかを判定し、落ち着くまで待ちます。
    766         *
    767         * FileUtil.stablePath( path , STABLE_SLEEP_TIME , STABLE_RETRY_COUNT ); と同じです。
    768         *
    769         * @param       path  チェックするパスオブジェクト
    770         * @return      true:安定した/false:安定しなかった。またはファイルが存在していない。
    771         * @see         #STABLE_SLEEP_TIME
    772         * @see         #STABLE_RETRY_COUNT
    773         */
    774        public static boolean stablePath( final Path path ) {
    775                return stablePath( path , STABLE_SLEEP_TIME , STABLE_RETRY_COUNT );
    776        }
    777
    778        /**
    779         * 指定のパスのファイルが、書き込まれている途中かどうかを判定し、落ち着くまで待ちます。
    780         *
    781         * ファイルの安定は、ファイルのサイズをチェックすることで求めます。まず、サイズをチェックし、
    782         * sleepで指定した時間だけ、Thread.sleepします。再び、サイズをチェックして、同じであれば、
    783         * 安定したとみなします。
    784         * なので、必ず、sleep で指定したミリ秒だけは、待ちます。
    785         * ファイルが存在しない、サイズが、０のままか、チェック回数を過ぎても安定しない場合は、
    786         * false が返ります。
    787         * サイズを求める際に、IOExceptionが発生した場合でも、falseを返します。
    788         *
    789         * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。
    790         *
    791         * @param       path  チェックするパスオブジェクト
    792         * @param       sleep 待機する時間(ミリ秒)
    793         * @param       cnt   チェックする回数
    794         * @return      true:安定した/false:安定しなかった。またはファイルが存在していない。
    795         */
    796        public static boolean stablePath( final Path path , final long sleep , final int cnt ) {
    797                // 存在しない場合は、即抜けます。
    798//              if( Files.exists( path ) ) {
    799                if( exists( path ) ) {                                  // 仮想ﾌｫﾙﾀﾞなどの場合、実態が存在しないことがある。
    800                        try{ Thread.sleep( sleep ); } catch( final InterruptedException ex ){}                          // 先に、無条件に待ちます。
    801                        try {
    802                                if( !exists( path ) ) { return false; }                                                                                 // 存在チェック。無ければ、false
    803                                long size1 = Files.size( path );                                                                                                // 7.3.1.3 (2021/03/09) forの前に移動
    804                                for( int i=0; i<cnt; i++ ) {
    805//                                      if( Files.notExists( path ) ) { return false; }                                                         // 存在チェック。無ければ、false
    806        //                              if( !exists( path ) ) { break; }                                                                                        // 存在チェック。無ければ、false
    807        //                              final long size1 = Files.size( path );                                                                          // exit point 警告が出ますが、Thread.sleep 前に、値を取得しておきたい。
    808
    809                                        try{ Thread.sleep( sleep ); } catch( final InterruptedException ex ){}          // 無条件に待ちます。
    810
    811//                                      if( Files.notExists( path ) ) { return false; }                                                         // 存在チェック。無ければ、false
    812                                        if( !exists( path ) ) { break; }                                                                                        // 存在チェック。無ければ、false
    813                                        final long size2 = Files.size( path );
    814                                        if( size1 != 0L && size1 == size2 ) { return true; }                                            // 安定した
    815                                        size1 = size2 ;                                                                                                                         // 7.3.1.3 (2021/03/09) 次のチェックループ
    816                                }
    817                        }
    818                        catch( final IOException ex ) {
    819                                // Exception は発生させません。
    820                                // MSG0005 = ﾌｫﾙﾀﾞのﾌｧｲﾙ読み込み時にｴﾗｰが発生しました。file=[{0}]
    821                                MsgUtil.errPrintln( ex , "MSG0005" , path );
    822                        }
    823                }
    824
    825                return false;
    826        }
    827
    828        /**
    829         * 指定のパスを共有ロックして、Consumer#action(Path) メソッドを実行します。
    830         * 共有ロック中は、ファイルを読み込むことは出来ますが、書き込むことは出来なくなります。
    831         *
    832         * 共有ロックの取得は、{@value #LOCK_RETRY_COUNT} 回実行し、{@value #LOCK_SLEEP_TIME} ミリ秒待機します。
    833         *
    834         * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。
    835         * @og.rev 7.4.4.0 (2021/06/30) NoSuchFileException 時は、メッセージのみ表示する。
    836         *
    837         * @param inPath        処理対象のPathオブジェクト
    838         * @param action        パスを引数に取るConsumerオブジェクト
    839         * @throws      RuntimeException ファイル読み込み時にエラーが発生した場合
    840         * @see         #forEach(Path,Consumer)
    841         * @see         #LOCK_RETRY_COUNT
    842         * @see         #LOCK_SLEEP_TIME
    843         */
    844        public static void lockPath( final Path inPath , final Consumer<Path> action ) {
    845                // 処理の直前で、処理対象のファイルが存在しているかどうか確認します。
    846//              if( Files.exists( inPath ) ) {
    847                if( exists( inPath ) ) {                                        // 7.2.5.0 (2020/06/01)
    848                        // try-with-resources 文 (AutoCloseable)
    849                        try( FileChannel channel = FileChannel.open( inPath, StandardOpenOption.READ ) ) {
    850                                 for( int i=0; i<LOCK_RETRY_COUNT; i++ ) {
    851                                        try {
    852                                                if( channel.tryLock( 0L,Long.MAX_VALUE,true ) != null ) {       // 共有ロック獲得成功
    853                                                        action.accept( inPath );
    854                                                        return;         // 共有ロック獲得成功したので、ループから抜ける。
    855                                                }
    856                                        }
    857                                        // 要求された領域をオーバーラップするロックがこのJava仮想マシンにすでに確保されている場合。
    858                                        // または、このメソッド内でブロックされている別のスレッドが同じファイルのオーバーラップした領域をロックしようとしている場合
    859                                        catch( final OverlappingFileLockException ex ) {
    860                //                              System.err.println( ex.getMessage() );
    861                                                if( i >= 3 ) {  // とりあえず３回までは、何も出さない
    862                                                        // MSG0104 = 要求された領域のﾛｯｸは、このJava仮想ﾏｼﾝにすでに確保されています。 \n\tfile=[{0}]
    863                //                                      LOGGER.warning( ex , "MSG0104" , inPath );
    864                                                        LOGGER.warning( "MSG0104" , inPath );                                   // 1.5.0 (2020/04/01) メッセージだけにしておきます。
    865                                                }
    866                                        }
    867                                        try{ Thread.sleep( LOCK_SLEEP_TIME ); } catch( final InterruptedException ex ){}
    868                                }
    869                        }
    870                        // 7.4.4.0 (2021/06/30) NoSuchFileException 時は、メッセージのみ表示する。
    871                        catch( final NoSuchFileException ex ) {
    872                                // MSG0002 = ﾌｧｲﾙ/ﾌｫﾙﾀﾞが存在しません。\n\tfile=[{0}]
    873                                LOGGER.warning( "MSG0002" , inPath );   // 原因不明：FileWatchとDirWatchの両方が動いているから？
    874                        }
    875                        catch( final IOException ex ) {
    876                                // MSG0005 = ﾌｫﾙﾀﾞのﾌｧｲﾙ読み込み時にｴﾗｰが発生しました。file=[{0}]
    877                                throw MsgUtil.throwException( ex , "MSG0005" , inPath );
    878                        }
    879
    880                        // Exception は発生させません。
    881                        // MSG0015 = ﾌｧｲﾙのﾛｯｸ取得に失敗しました。file=[{0}] WAIT=[{1}](ms) COUNT=[{2}]
    882//                      MsgUtil.errPrintln( "MSG0015" , inPath , LOCK_SLEEP_TIME , LOCK_RETRY_COUNT );
    883                        LOGGER.warning( "MSG0015" , inPath , LOCK_SLEEP_TIME , LOCK_RETRY_COUNT );
    884                }
    885        }
    886
    887        /**
    888         * 指定のパスから、１行づつ読み取った結果をConsumerにセットする繰り返しメソッドです。
    889         * １行単位に、Consumer#action が呼ばれます。
    890         * このメソッドでは、Charset は、UTF-8 です。
    891         *
    892         * ファイルを順次読み込むため、内部メモリを圧迫しません。
    893         *
    894         * @param inPath        処理対象のPathオブジェクト
    895         * @param action        行を引数に取るConsumerオブジェクト
    896         * @throws      RuntimeException ファイル読み込み時にエラーが発生した場合
    897         * @see         #lockForEach(Path,Consumer)
    898         */
    899        public static void forEach( final Path inPath , final Consumer<String> action ) {
    900                forEach( inPath , UTF_8 , action );
    901        }
    902
    903        /**
    904         * 指定のパスから、１行づつ読み取った結果をConsumerにセットする繰り返しメソッドです。
    905         * １行単位に、Consumer#action が呼ばれます。
    906         *
    907         * ファイルを順次読み込むため、内部メモリを圧迫しません。
    908         *
    909         * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。
    910         *
    911         * @param inPath        処理対象のPathオブジェクト
    912         * @param chset         ファイルを読み取るときのCharset
    913         * @param action        行を引数に取るConsumerオブジェクト
    914         * @throws      RuntimeException ファイル読み込み時にエラーが発生した場合
    915         * @see         #lockForEach(Path,Consumer)
    916         */
    917        public static void forEach( final Path inPath , final Charset chset , final Consumer<String> action ) {
    918                // 処理の直前で、処理対象のファイルが存在しているかどうか確認します。
    919//              if( Files.exists( inPath ) ) {
    920                if( exists( inPath ) ) {                                        // 7.2.5.0 (2020/06/01)
    921                        // try-with-resources 文 (AutoCloseable)
    922                        String line = null;
    923                        int no = 0;
    924        //              // こちらの方法では、lockForEach から来た場合に、エラーになります。
    925        //              try( BufferedReader reader = Files.newBufferedReader( inPath , chset ) ) {
    926                        // 万一、コンストラクタでエラーが発生すると、リソース開放されない場合があるため、個別にインスタンスを作成しておきます。(念のため)
    927                        try( FileInputStream   fin = new FileInputStream( inPath.toFile() );
    928                                 InputStreamReader isr = new InputStreamReader( fin , chset );
    929                                 BufferedReader reader = new BufferedReader( isr ) ) {
    930
    931                                while( ( line = reader.readLine() ) != null ) {
    932                                        // 1.2.0 (2018/09/01) UTF-8 BOM 対策
    933                                        // UTF-8 の BOM(0xEF 0xBB 0xBF) は、Java内部文字コードの UTF-16 BE では、0xFE 0xFF になる。
    934                                        // ファイルの先頭文字が、feff の場合は、その文字を削除します。
    935                        //              if( no == 0 && !line.isEmpty() && Integer.toHexString(line.charAt(0)).equalsIgnoreCase("feff") ) {
    936                                        if( no == 0 && !line.isEmpty() && (int)line.charAt(0) == (int)'\ufeff' ) {
    937                                                // MSG0105 = 指定のﾌｧｲﾙは、UTF-8 BOM付きです。BOM無しﾌｧｲﾙで、運用してください。 \n\tfile=[{0}]
    938                                                System.out.println( MsgUtil.getMsg( "MSG0105" , inPath ) );
    939                                                line = line.substring(1);                       // BOM の削除 : String#replace("\ufeff","") の方が良い？
    940                                        }
    941
    942                                        action.accept( line );
    943                                        no++;
    944                                }
    945                        }
    946                        catch( final IOException ex ) {
    947                                // MSG0016 = ﾌｧｲﾙの行ﾃﾞｰﾀ読み込みに失敗しました。\n\tfile={0} , 行番号:{1} , 行:{2}
    948                                throw MsgUtil.throwException( ex , "MSG0016" , inPath , no , line );
    949                        }
    950                }
    951        }
    952
    953        /**
    954         * 指定のパスを共有ロックして、１行づつ読み取った結果をConsumerにセットする繰り返しメソッドです。
    955         * １行単位に、Consumer#action が呼ばれます。
    956         *
    957         * ファイルを順次読み込むため、内部メモリを圧迫しません。
    958         *
    959         * @param inPath        処理対象のPathオブジェクト
    960         * @param action        行を引数に取るConsumerオブジェクト
    961         * @see         #forEach(Path,Consumer)
    962         */
    963        public static void lockForEach( final Path inPath , final Consumer<String> action ) {
    964                lockPath( inPath , in -> forEach( in , UTF_8 , action ) );
    965        }
    966
    967        /**
    968         * 指定のパスを共有ロックして、１行づつ読み取った結果をConsumerにセットする繰り返しメソッドです。
    969         * １行単位に、Consumer#action が呼ばれます。
    970         *
    971         * ファイルを順次読み込むため、内部メモリを圧迫しません。
    972         *
    973         * @param inPath        処理対象のPathオブジェクト
    974         * @param chset         エンコードを指定するCharsetオブジェクト
    975         * @param action        行を引数に取るConsumerオブジェクト
    976         * @see         #forEach(Path,Consumer)
    977         */
    978        public static void lockForEach( final Path inPath , final Charset chset , final Consumer<String> action ) {
    979                lockPath( inPath , in -> forEach( in , chset , action ) );
    980        }
    981
    982        /**
    983         * 指定のパスに１行単位の文字列のListを書き込んでいきます。
    984         * １行単位の文字列のListを作成しますので、大きなファイルの作成には向いていません。
    985         *
    986         * 書き込むパスの親フォルダがなければ作成します。
    987         * 第２引数は、書き込む行データです。
    988         * このメソッドでは、Charset は、UTF-8 です。
    989         *
    990         * @og.rev 1.0.0 (2016/04/28) 新規追加
    991         *
    992         * @param       savePath セーブするパスオブジェクト
    993         * @param       lines   行単位の書き込むデータ
    994         * @throws      RuntimeException ファイル操作に失敗した場合
    995         * @see         #save( Path , List , boolean , Charset )
    996         */
    997        public static void save( final Path savePath , final List<String> lines ) {
    998                save( savePath , lines , false , UTF_8 );               // 新規作成
    999        }
    1000
    1001        /**
    1002         * 指定のパスに１行単位の文字列のListを書き込んでいきます。
    1003         * １行単位の文字列のListを作成しますので、大きなファイルの作成には向いていません。
    1004         *
    1005         * 書き込むパスの親フォルダがなければ作成します。
    1006         *
    1007         * 第２引数は、書き込む行データです。
    1008         *
    1009         * @og.rev 1.0.0 (2016/04/28) 新規追加
    1010         * @og.rev 7.2.5.0 (2020/06/01) BOM付きファイルを append する場合の対処
    1011         * @og.rev 8.0.0.0 (2021/07/01) STATIC_LOCKのsynchronized作成
    1012         *
    1013         * @param       savePath セーブするパスオブジェクト
    1014         * @param       lines   行単位の書き込むデータ
    1015         * @param       append  trueの場合、ファイルの先頭ではなく最後に書き込まれる。
    1016         * @param       chset   ファイルを読み取るときのCharset
    1017         * @throws      RuntimeException ファイル操作に失敗した場合
    1018         */
    1019        public static void save( final Path savePath , final List<String> lines , final boolean append , final Charset chset ) {
    1020                // 6.9.8.0 (2018/05/28) FindBugs:null になっている可能性があるメソッドの戻り値を利用している
    1021                // ※ toAbsolutePath() する必要はないのと、getParent() は、null を返すことがある
    1022//              mkdirs( savePath.toAbsolutePath().getParent() );                // savePathはファイルなので、親フォルダを作成する。
    1023                final Path parent = savePath.getParent();
    1024                if( parent == null ) {
    1025                        // MSG0007 = ﾌｧｲﾙ/ﾌｫﾙﾀﾞの作成に失敗しました。\n\tdir=[{0}]
    1026                        throw MsgUtil.throwException( "MSG0007" , savePath.toString() );
    1027                }
    1028                else {
    1029                        mkdirs( parent,false );
    1030                }
    1031
    1032                String line = null;             // エラー出力のための変数
    1033                int no = 0;
    1034
    1035                synchronized( STATIC_LOCK ) {
    1036                        // try-with-resources 文 (AutoCloseable)
    1037                        try( PrintWriter out = new PrintWriter( Files.newBufferedWriter( savePath, chset , append ? APPEND : CREATE ) ) ) {
    1038                                 for( final String ln : lines ) {
    1039        //                              line = ln ;
    1040                                        // 7.2.5.0 (2020/06/01) BOM付きファイルを append する場合の対処
    1041                                        if( !ln.isEmpty() && (int)ln.charAt(0) == (int)'\ufeff' ) {
    1042                                                line = ln.substring(1);                 // BOM の削除 : String#replace("\ufeff","") の方が良い？
    1043                                        }
    1044                                        else {
    1045                                                line = ln ;
    1046                                        }
    1047                                        no++;
    1048                                        out.println( line );
    1049                                }
    1050                                out.flush();
    1051                        }
    1052                        catch( final IOException ex ) {
    1053                                // MSG0017 = ﾌｧｲﾙのﾃﾞｰﾀ書き込みに失敗しました。file:行番号:行\n\t{0}:{1}: {2}
    1054                                throw MsgUtil.throwException( ex , "MSG0017" , savePath , no , line );
    1055                        }
    1056                }
    1057        }
    1058
    1059        /**
    1060         * 指定のパスの最終更新日付を、文字列で返します。
    1061         * 文字列のフォーマット指定も可能です。
    1062         *
    1063         * パスが無い場合や、最終更新日付を、取得できない場合は、現在時刻をベースに返します。
    1064         *
    1065         * @og.rev 7.2.5.0 (2020/06/01) ネットワークパスのチェックを行います。
    1066         * @og.rev 8.0.0.0 (2021/07/01) STATIC_LOCKのsynchronized作成
    1067         *
    1068         * @param path          処理対象のPathオブジェクト
    1069         * @param format        文字列化する場合のフォーマット(yyyyMMddHHmmss)
    1070         * @return      指定のパスの最終更新日付の文字列
    1071         */
    1072        public static String timeStamp( final Path path , final String format ) {
    1073                long tempTime = 0L;
    1074                try {
    1075                        // 存在チェックを直前に入れますが、厳密には、非同期なので確率の問題です。
    1076//                      if( Files.exists( path ) ) {
    1077                        if( exists( path ) ) {                                  // 7.2.5.0 (2020/06/01)
    1078                                synchronized( STATIC_LOCK ) {
    1079                                        tempTime = Files.getLastModifiedTime( path ).toMillis();
    1080                                }
    1081                        }
    1082                }
    1083                catch( final IOException ex ) {
    1084                        // MSG0018 = ﾌｧｲﾙのﾀｲﾑｽﾀﾝﾌﾟの取得に失敗しました。file=[{0}]
    1085//                      MsgUtil.errPrintln( ex , "MSG0018" , path , ex.getMessage() );
    1086                        // MSG0018 = ﾌｧｲﾙのﾀｲﾑｽﾀﾝﾌﾟの取得に失敗しました。\n\tfile=[{0}]
    1087                        LOGGER.warning( ex , "MSG0018" , path );
    1088                }
    1089                if( tempTime == 0L ) {
    1090                        tempTime = System.currentTimeMillis();          // パスが無い場合や、エラー時は、現在時刻を使用
    1091                }
    1092
    1093                return StringUtil.getTimeFormat( tempTime , format );
    1094        }
    1095
    1096        /**
    1097         * ファイルからすべての行を読み取って、文字列のListとして返します。
    1098         *
    1099         * java.nio.file.Files#readAllLines​(Path ) と同等ですが、ﾌｧｲﾙが UTF-8 でない場合
    1100         * 即座にエラーにするのではなく、Windows-31J でも読み取りを試みます。
    1101         * それでもダメな場合は、IOException をスローします。
    1102         *
    1103         * @og.rev 7.2.5.0 (2020/06/01) Files.readAllLines の代用
    1104         * @og.rev 7.3.1.3 (2021/03/09) 読み込み処理全体に、try ～ catch を掛けておきます。
    1105         * @og.rev 8.0.0.0 (2021/07/01) STATIC_LOCKのsynchronized作成
    1106         *
    1107         * @param path          読み取り対象のPathオブジェクト
    1108         * @return      Listとしてファイルからの行
    1109         * @throws      IOException 読み取れない場合エラー
    1110         */
    1111        public static List<String> readAllLines( final Path path ) throws IOException {
    1112                // 7.3.1.3 (2021/03/09) 読み込み処理全体に、try ～ catch を掛けておきます。
    1113                try {
    1114                        synchronized( STATIC_LOCK ) {
    1115                                try {
    1116                                        return Files.readAllLines( path );                              // StandardCharsets.UTF_8 指定と同等。
    1117                                }
    1118                                catch( final MalformedInputException ex ) {
    1119                                        // MSG0030 = 指定のﾌｧｲﾙは、UTF-8でｵｰﾌﾟﾝ出来なかったため、Windows-31J で再実行します。\n\tfile=[{0}]
    1120                                        LOGGER.warning( "MSG0030" , path );                             // Exception は、引数に渡さないでおきます。
    1121
    1122                                        return Files.readAllLines( path,WINDOWS_31J );
    1123                                }
    1124                        }
    1125                }
    1126                catch( final IOException ex ) {
    1127                        // MSG0005 = ﾌｫﾙﾀﾞのﾌｧｲﾙ読み込み時にｴﾗｰが発生しました。file=[{0}]
    1128                        throw MsgUtil.throwException( ex , "MSG0005" , path );
    1129                }
    1130        }
    1131
    1132        /**
    1133         * Pathオブジェクトが存在しているかどうかを判定します。
    1134         *
    1135         * java.nio.file.Files#exists( Path ) を使用せず、java.io.File.exists() で判定します。
    1136         * https://codeday.me/jp/qa/20190302/349168.html
    1137         * ネットワークフォルダに存在するファイルの判定において、Files#exists( Path )と
    1138         * File.exists() の結果が異なることがあります。
    1139         * ここでは、File#exists() を使用して判定します。
    1140         *
    1141         * @og.rev 7.2.5.0 (2020/06/01) Files.exists の代用
    1142         *
    1143         * @param path          判定対象のPathオブジェクト
    1144         * @return      ファイルの存在チェック(あればtrue)
    1145         */
    1146        public static boolean exists( final Path path ) {
    1147        //      return Files.exists( path );
    1148                return path != null && path.toFile().exists();
    1149        }
    1150
    1151        /**
    1152         * Pathオブジェクトのﾌｧｲﾙ名(getFileName().toString()) を取得します。
    1153         *
    1154         * Path#getFileName() では、結果が null になる場合もあり、そのままでは、toString() できません。
    1155         * また、引数の Path も null チェックが必要なので、それらを簡易的に行います。
    1156         * 何らかの結果が、null の場合は、""(空文字列)を返します。
    1157         *
    1158         * @og.rev 7.2.9.4 (2020/11/20) Path.getFileName().toString() の簡易版
    1159         *
    1160         * @param path          ﾌｧｲﾙ名取得元のPathオブジェクト(nullも可)
    1161         * @return      ﾌｧｲﾙ名(nullの場合は、空文字列)
    1162         * @og.rtnNotNull
    1163         */
    1164        public static String pathFileName( final Path path ) {
    1165                // 対応済み：spotbugs:null になっている可能性があるメソッドの戻り値を利用している
    1166//              return path == null || path.getFileName() == null ? "" : path.getFileName().toString();
    1167
    1168                if( path != null ) {
    1169                        final Path fname = path.getFileName();
    1170                        if( fname != null ) {
    1171                                return fname.toString();
    1172                        }
    1173                }
    1174                return "" ;
    1175        }
    1176}